diff --git a/Makefile b/Makefile index 63557f7087aabf66b1061fa96339ba268ba73431..88f9f38a0b3d64f69e168c089014251631a27284 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-hostname-checker-app +build: pre build-gosdn build-gosdnc build-orchestrator build-venv-manager build-arista-routing-engine-app build-hostname-checker-app build-basic-interface-monitoring-app build-gosdn: pre $(GOBUILD) -trimpath -o $(BUILD_ARTIFACTS_PATH)/gosdn ./controller/cmd/gosdn @@ -76,6 +76,9 @@ build-arista-routing-engine-app: pre build-hostname-checker-app: pre $(GOBUILD) -trimpath -o $(BUILD_ARTIFACTS_PATH)/hostname-checker ./applications/hostname-checker +build-basic-interface-monitoring-app: pre + $(GOBUILD) -trimpath -o $(BUILD_ARTIFACTS_PATH)/basic-interface-monitoring ./applications/basic-interface-monitoring + containerize-all: containerize-gosdn containerize-gosdnc containerize-orchestrator containerize-target containerize-gosdn: diff --git a/application-framework/event/eventService.go b/application-framework/event/eventService.go index b71c0541e790124603cc4d3f6dc65123d5fa70fc..9edf6b7096a22f59b17dbd6092db15df8874df55 100644 --- a/application-framework/event/eventService.go +++ b/application-framework/event/eventService.go @@ -115,9 +115,7 @@ func (e *Service) setupQueueConsume(topic string, stopChan chan os.Signal) error val, ok := e.subscriber[parseTypeString(event.Type)] if ok { - fmt.Print("Executing Callback: ") val(event) - fmt.Println() } } }() diff --git a/applications/basic-interface-monitoring/app.go b/applications/basic-interface-monitoring/app.go new file mode 100644 index 0000000000000000000000000000000000000000..9f82cca697694241c8016bfec2df56fd016632ce --- /dev/null +++ b/applications/basic-interface-monitoring/app.go @@ -0,0 +1,76 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement" + + "code.fbi.h-da.de/danet/gosdn/application-framework/event" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +// Application is an example for a sdn application. +type Application struct { + eventServiceNetworkElements event.ServiceInterface + stopChannel chan os.Signal + grpcClientConn *grpc.ClientConn +} + +// Run runs the application. +func (a *Application) Run(controllerAddress string) { + statusMap = make(map[string]map[string]*InterfaceStatus) + + signal.Notify(a.stopChannel, os.Interrupt, syscall.SIGTERM) + + a.eventServiceNetworkElements.SubscribeToEventType([]event.TypeToCallbackTuple{ + {Type: event.Update, Callback: a.NetworkElementCallback}, + }) + a.eventServiceNetworkElements.SetupEventReciever(a.stopChannel) + + conn, err := grpc.Dial(controllerAddress, 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 + } + } + }() + + if err := StartHTTPServer(); err != nil { + panic(err) + } + + <-forever +} + +// NetworkElementCallback is the callback function for network element changes. +func (a *Application) NetworkElementCallback(event *event.Event) { + networkElementServer := networkelement.NewNetworkElementServiceClient(a.grpcClientConn) + + changedInterfaces, err := checkIfOperationStateHasChanged(networkElementServer, event.EntityID) + if err != nil { + fmt.Printf("Error %+v\n ", err) + } + + if changedInterfaces != nil { + for _, changedInterface := range changedInterfaces { + fmt.Printf("Change on %s: status of interface %s has changed to %s\n", changedInterface.NetworkElementName, changedInterface.Name, changedInterface.Status) + } + } +} diff --git a/applications/basic-interface-monitoring/http.go b/applications/basic-interface-monitoring/http.go new file mode 100644 index 0000000000000000000000000000000000000000..6e7d35f868958bbcc07290d5ec9664c87e7571b3 --- /dev/null +++ b/applications/basic-interface-monitoring/http.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "net/http" +) + +// based on https://community.hetzner.com/tutorials/real-time-apps-with-go-and-reactjs/server-sent-events + +var clientChannels = make(map[chan []byte]bool) + +// StartHTTPServer starts the HTTP server to provide the monitoring page. +func StartHTTPServer() error { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, indexPath) + }) + http.HandleFunc("/sse", sseHandler) + + if err := http.ListenAndServe(":4000", nil); err != nil { + return err + } + + return nil +} + +func sseHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Access-Control-Allow-Origin", "*") + + clientChannel := make(chan []byte) + clientChannels[clientChannel] = true + + for { + select { + case update := <-clientChannel: + test := fmt.Sprintf("data: %s\n\n", string(update)) + w.Write([]byte(test)) + w.(http.Flusher).Flush() + case <-r.Context().Done(): + delete(clientChannels, clientChannel) + return + } + } +} diff --git a/applications/basic-interface-monitoring/main.go b/applications/basic-interface-monitoring/main.go new file mode 100644 index 0000000000000000000000000000000000000000..5f451c99a2287e7be7a3e3cd46b42c6dc39d6f5e --- /dev/null +++ b/applications/basic-interface-monitoring/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "flag" + "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" +) + +var controllerAddress string +var indexPath string + +func main() { + flag.StringVar(&indexPath, "path", "webpage/index.html", "path to the webpage index.html file") + flag.StringVar(&controllerAddress, "controller-address", "localhost:55055", "the address to the controller") + + flag.Parse() + + queueCredentials, err := registration.Register(controllerAddress, "basic-interface-monitoring", "SecurePresharedToken") + if err != nil { + logrus.Errorf("failed to register application on control plane. %v", err) + os.Exit(1) + } + + eventServiceNetworkElements, err := event.NewEventService( + queueCredentials, + []event.Topic{event.ManagedNetworkElement}, + ) + if err != nil { + logrus.Errorf("failed to create event service. %v", err) + os.Exit(1) + } + + app := &Application{ + eventServiceNetworkElements: eventServiceNetworkElements, + stopChannel: make(chan os.Signal, 1), + } + + app.Run(controllerAddress) +} diff --git a/applications/basic-interface-monitoring/network-element.go b/applications/basic-interface-monitoring/network-element.go new file mode 100644 index 0000000000000000000000000000000000000000..c2993a25a680e46528133f4123d185dd3710fa63 --- /dev/null +++ b/applications/basic-interface-monitoring/network-element.go @@ -0,0 +1,137 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement" + "code.fbi.h-da.de/danet/gosdn/application-framework/models" + "code.fbi.h-da.de/danet/gosdn/models/generated/arista" + + "github.com/google/uuid" + "github.com/openconfig/ygot/ygot" + "github.com/openconfig/ygot/ytypes" +) + +var statusMap map[string]map[string]*InterfaceStatus + +// NetworkElement is a NetworkElement. +type NetworkElement struct { + // UUID represents the network element's UUID + UUID uuid.UUID + + // Name is the network element's human readable Name + Name string + + // Model embeds a ygot.GoStruct containing the network element details + Model arista.Device +} + +// NewNetworkElement creates a new NetworkElement. +func NewNetworkElement(id uuid.UUID, name string, networkElementModel string) *NetworkElement { + d := &NetworkElement{ + UUID: id, + Model: arista.Device{}, + Name: 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) + } + + opts := []ytypes.UnmarshalOpt{ + &ytypes.IgnoreExtraFields{}, + } + // Use unmarshall from the network elements model to unmarshall ygot json in go struct. + err = models.Unmarshal([]byte(networkElementModel), path, &d.Model, opts...) + if err != nil { + panic(err) + } + + return d +} + +// InterfaceStatus contains information of an interface. +type InterfaceStatus struct { + NetworkElementName string + Name string + Status string +} + +func checkIfOperationStateHasChanged(networkElementServer networkelement.NetworkElementServiceClient, networkElementID uuid.UUID) ([]InterfaceStatus, error) { + ctx := context.Background() + + request := &networkelement.GetNetworkElementRequest{ + Timestamp: time.Now().UnixNano(), + NetworkElementId: networkElementID.String(), + } + + resp, err := networkElementServer.Get(ctx, request) + if err != nil { + return nil, err + } + + networkElement := NewNetworkElement(uuid.MustParse(resp.NetworkElement.Id), resp.NetworkElement.Name, resp.NetworkElement.Model) + + storedInterfaces, ok := statusMap[networkElement.Name] + if !ok { + addNetworkElementToStatusMap(networkElement.Name, networkElement.Model.Interfaces.Interface) + return nil, nil + } + + return walkThroughInterfaces(networkElement.Model.Interfaces.Interface, storedInterfaces) +} + +func addNetworkElementToStatusMap(networkElementName string, interfaces map[string]*arista.OpenconfigInterfaces_Interfaces_Interface) { + statusList := make(map[string]*InterfaceStatus) + + for _, receivedInterface := range interfaces { + statusList[*receivedInterface.Name] = &InterfaceStatus{ + NetworkElementName: networkElementName, + Name: *receivedInterface.Name, + Status: receivedInterface.State.OperStatus.String(), + } + } + + statusMap[networkElementName] = statusList +} + +func walkThroughInterfaces(interfaces map[string]*arista.OpenconfigInterfaces_Interfaces_Interface, storedInterfaces map[string]*InterfaceStatus) ([]InterfaceStatus, error) { + statusList := make([]InterfaceStatus, 0) + + for _, receivedInterface := range interfaces { + storedInterface, ok := storedInterfaces[*receivedInterface.Name] + if !ok { + return statusList, fmt.Errorf("could not find %s in stored interfaces", *receivedInterface.Name) + } + + if storedInterface.Status != receivedInterface.State.OperStatus.String() { + statusList = append(statusList, InterfaceStatus{ + NetworkElementName: storedInterface.NetworkElementName, + Name: *receivedInterface.Name, + Status: receivedInterface.State.OperStatus.String(), + }) + + storedInterface.Status = receivedInterface.State.OperStatus.String() + + statusMapCopy := statusMap + + go func() { + b, err := json.Marshal(statusMapCopy) + if err != nil { + fmt.Println("error: ", err) + return + } + for clientChannel := range clientChannels { + clientChannel <- []byte(b) + } + }() + } + } + + return statusList, nil +} diff --git a/applications/basic-interface-monitoring/webpage/index.html b/applications/basic-interface-monitoring/webpage/index.html new file mode 100644 index 0000000000000000000000000000000000000000..7e8d3fc2d528471a8bed7950a2059f87fb90d6ed --- /dev/null +++ b/applications/basic-interface-monitoring/webpage/index.html @@ -0,0 +1,111 @@ +<script type="text/javascript"> + let lastStateNetworkElementMap = new Map() + + const eventListener = new EventSource("http://localhost:4000/sse") + eventListener.onmessage = (event) => { + const content = document.querySelector('.content'); + + data = JSON.parse(event.data); + + contentInnerHTML = ""; + + for (let networkElementName in data) { + let interfaces = data[networkElementName]; + + contentInnerHTML += "<h1>" + networkElementName + "</h1>"; + + contentInnerHTML += '<div class="flex-container">'; + + let interfaceArray = new Array() + for (let interfaceName in interfaces) { + interfaceArray.push(interfaces[interfaceName]) + } + + interfaceArray = interfaceArray.filter((interface) => { + return !interface.Name.includes("/") + }) + + interfaceArray.sort((a,b) => { + interfaceA = parseInt(a.Name.split("Ethernet")[1]) + interfaceB = parseInt(b.Name.split("Ethernet")[1]) + return interfaceB < interfaceA + }) + + console.log(lastStateNetworkElementMap.get(networkElementName)) + + + const lastStateArray = lastStateNetworkElementMap.get(networkElementName) + + interfaceArray.forEach( (interface, index) => { + contentInnerHTML += "<div" + if (lastStateArray != undefined) { + if (interface.Status != lastStateArray[index].Status) { + contentInnerHTML += ` class="changed"` + } + } + contentInnerHTML += "><p>" + interface.Name + "</p>"; + if (interface.Status == "UP") { + contentInnerHTML += '<p class="green">' + interface.Status + "</p>"; + } else { + contentInnerHTML += '<p class="red">' + interface.Status + "</p>"; + } + contentInnerHTML += "</div>"; + }) + + lastStateNetworkElementMap.set(networkElementName, interfaceArray) + + contentInnerHTML += "</div>"; + } + + content.innerHTML = contentInnerHTML; + } +</script> + +<!DOCTYPE html> +<html> +<head> +<style> +.flex-container { + display: flex; + flex-wrap: wrap; +} + +.flex-container > div { + background-color: grey; + width: 250px; + margin: 10px; + text-align: center; + font-size: 30px; +} + +.red { + color: white; + background-color: red; +} + +.green { + color: white; + background-color: green; +} + +.changed { + outline:10px dashed transparent; + animation: changed 1s step-end 12; +} + +@keyframes changed { + from, to { + outline-color: transparent + } + 50% { + outline-color: blue + } +} +</style> +</head> +<body> + +<div class="content"></div> + +</body> +</html> diff --git a/go.mod b/go.mod index 96832340527fb2e276790b86347497b971aa9c11..ebd186120a587195b9340d514202208c5374b69c 100644 --- a/go.mod +++ b/go.mod @@ -90,7 +90,7 @@ require ( go.opencensus.io v0.23.0 // indirect golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa golang.org/x/net v0.0.0-20220728030405-41545e8bf201 - golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect + golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/ini.v1 v1.66.6 // indirect diff --git a/go.sum b/go.sum index 0b4b959c6f001f7bc317e200ddabe2f4b67bc521..58d7445567bb55cbf4f109bb8fb4486f93cc7f4c 100644 --- a/go.sum +++ b/go.sum @@ -1282,8 +1282,8 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM= +golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=