From 856d55d6413fe66c05b42a3c5e98d0b0f20743e3 Mon Sep 17 00:00:00 2001 From: elinuxhenrik Date: Tue, 24 Aug 2021 17:01:24 +0200 Subject: [PATCH] First go version of o-ru-closed-loop Issue-ID: NONRTRIC-588 Signed-off-by: elinuxhenrik Change-Id: I41390ec58eb8281d87b92da8b0893666aa00ae3e --- .../oruclosedlooprecovery/goversion/.gitignore | 4 + .../oruclosedlooprecovery/goversion/Dockerfile | 30 +++ .../build-oruclosedloopconsumer-ubuntu.sh | 40 ++++ .../goversion/container-tag.yaml | 5 + .../oruclosedlooprecovery/goversion/go.mod | 18 ++ .../oruclosedlooprecovery/goversion/go.sum | 23 ++ .../goversion/internal/config/config.go | 84 +++++++ .../goversion/internal/config/config_test.go | 119 ++++++++++ .../internal/linkfailure/linkfailurehandler.go | 113 +++++++++ .../linkfailure/linkfailurehandler_test.go | 183 ++++++++++++++ .../goversion/internal/repository/csvhelp.go | 51 ++++ .../goversion/internal/repository/csvhelp_test.go | 82 +++++++ .../goversion/internal/repository/lookupservice.go | 73 ++++++ .../internal/repository/lookupservice_test.go | 176 ++++++++++++++ .../goversion/internal/restclient/client.go | 116 +++++++++ .../goversion/internal/ves/decoder.go | 42 ++++ .../goversion/internal/ves/decoder_test.go | 64 +++++ .../goversion/internal/ves/message.go | 64 +++++ .../goversion/internal/ves/message_test.go | 262 +++++++++++++++++++++ .../oruclosedlooprecovery/goversion/main.go | 109 +++++++++ .../goversion/mocks/CsvFileHelper.go | 33 +++ .../goversion/mocks/HTTPClient.go | 60 +++++ .../goversion/mocks/LookupService.go | 45 ++++ .../goversion/o-ru-to-o-du-map.csv | 11 + 24 files changed, 1807 insertions(+) create mode 100644 test/usecases/oruclosedlooprecovery/goversion/.gitignore create mode 100644 test/usecases/oruclosedlooprecovery/goversion/Dockerfile create mode 100755 test/usecases/oruclosedlooprecovery/goversion/build-oruclosedloopconsumer-ubuntu.sh create mode 100644 test/usecases/oruclosedlooprecovery/goversion/container-tag.yaml create mode 100644 test/usecases/oruclosedlooprecovery/goversion/go.mod create mode 100644 test/usecases/oruclosedlooprecovery/goversion/go.sum create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/config/config.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/config/config_test.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/linkfailure/linkfailurehandler.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/linkfailure/linkfailurehandler_test.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/repository/csvhelp.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/repository/csvhelp_test.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/repository/lookupservice.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/repository/lookupservice_test.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/restclient/client.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/ves/decoder.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/ves/decoder_test.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/ves/message.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/internal/ves/message_test.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/main.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/mocks/CsvFileHelper.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/mocks/HTTPClient.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/mocks/LookupService.go create mode 100644 test/usecases/oruclosedlooprecovery/goversion/o-ru-to-o-du-map.csv diff --git a/test/usecases/oruclosedlooprecovery/goversion/.gitignore b/test/usecases/oruclosedlooprecovery/goversion/.gitignore new file mode 100644 index 00000000..06758a79 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/.gitignore @@ -0,0 +1,4 @@ +*.out +.history + +oruclosedloop diff --git a/test/usecases/oruclosedlooprecovery/goversion/Dockerfile b/test/usecases/oruclosedlooprecovery/goversion/Dockerfile new file mode 100644 index 00000000..2462c442 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/Dockerfile @@ -0,0 +1,30 @@ +## +## Build +## +FROM golang:1.17.1-bullseye AS build + +WORKDIR /app + +COPY go.mod ./ +COPY go.sum ./ +RUN go mod download + +COPY . ./ + +RUN go build -o /docker-oruclosedloop + +## +## Deploy +## +FROM gcr.io/distroless/base-debian10 + +WORKDIR / + +## Copy from "build" stage +COPY --from=build /docker-oruclosedloop . + +COPY --from=build /app/o-ru-to-o-du-map.csv . + +USER nonroot:nonroot + +ENTRYPOINT ["/docker-oruclosedloop"] diff --git a/test/usecases/oruclosedlooprecovery/goversion/build-oruclosedloopconsumer-ubuntu.sh b/test/usecases/oruclosedlooprecovery/goversion/build-oruclosedloopconsumer-ubuntu.sh new file mode 100755 index 00000000..225fbb54 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/build-oruclosedloopconsumer-ubuntu.sh @@ -0,0 +1,40 @@ +#!/bin/bash +############################################################################## +# +# Copyright (C) 2021: Nordix Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################## +set -eux + +echo "--> build-oruclosedloopconsumer-ubuntu.sh" +curdir=`pwd` +# go installs tools like go-acc to $HOME/go/bin +# ubuntu minion path lacks go +export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin +go version +cd test/usecases/oruclosedlooprecovery/goversion/ + +# install the go coverage tool helper +go get -v github.com/ory/go-acc + +export GO111MODULE=on +go get github.com/stretchr/testify/mock@v1.7.0 + +go-acc ./... --ignore mocks + +sed -i -e 's/oransc\.org\/usecase\/oruclosedloop\///g' coverage.txt + +cp coverage.txt $curdir +echo "--> build-oruclosedloopconsumer-ubuntu.sh ends" diff --git a/test/usecases/oruclosedlooprecovery/goversion/container-tag.yaml b/test/usecases/oruclosedlooprecovery/goversion/container-tag.yaml new file mode 100644 index 00000000..6b1c9db7 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/container-tag.yaml @@ -0,0 +1,5 @@ +# The Jenkins job requires a tag to build the Docker image. +# By default this file is in the docker build directory, +# but the location can configured in the JJB template. +--- +tag: 1.0.0 diff --git a/test/usecases/oruclosedlooprecovery/goversion/go.mod b/test/usecases/oruclosedlooprecovery/goversion/go.mod new file mode 100644 index 00000000..754bba10 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/go.mod @@ -0,0 +1,18 @@ +module oransc.org/usecase/oruclosedloop + +go 1.17 + +require ( + github.com/sirupsen/logrus v1.8.1 + github.com/stretchr/testify v1.7.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.1.1 // indirect + golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/test/usecases/oruclosedlooprecovery/goversion/go.sum b/test/usecases/oruclosedlooprecovery/goversion/go.sum new file mode 100644 index 00000000..6ce76047 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/go.sum @@ -0,0 +1,23 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/config/config.go b/test/usecases/oruclosedlooprecovery/goversion/internal/config/config.go new file mode 100644 index 00000000..43656b77 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/config/config.go @@ -0,0 +1,84 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package config + +import ( + "os" + "strconv" + + log "github.com/sirupsen/logrus" +) + +type Config struct { + LogLevel log.Level + ConsumerHost string + ConsumerPort int + InfoCoordinatorAddress string + SDNRHost string + SDNRPort int + SDNRUser string + SDNPassword string + ORUToODUMapFile string +} + +func New() *Config { + return &Config{ + LogLevel: getLogLevel(), + ConsumerHost: getEnv("CONSUMER_HOST", ""), + ConsumerPort: getEnvAsInt("CONSUMER_PORT", 0), + InfoCoordinatorAddress: getEnv("INFO_COORD_ADDR", "http://enrichmentservice:8083"), + SDNRHost: getEnv("SDNR_HOST", "http://localhost"), + SDNRPort: getEnvAsInt("SDNR_PORT", 3904), + SDNRUser: getEnv("SDNR_USER", "admin"), + SDNPassword: getEnv("SDNR_PASSWORD", "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U"), + ORUToODUMapFile: getEnv("ORU_TO_ODU_MAP_FILE", "o-ru-to-o-du-map.csv"), + } +} + +func getEnv(key string, defaultVal string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + + return defaultVal +} + +func getEnvAsInt(name string, defaultVal int) int { + valueStr := getEnv(name, "") + if value, err := strconv.Atoi(valueStr); err == nil { + return value + } else if valueStr != "" { + log.Warnf("Invalid int value: %v for variable: %v. Default value: %v will be used", valueStr, name, defaultVal) + } + + return defaultVal +} + +func getLogLevel() log.Level { + logLevelStr := getEnv("LOG_LEVEL", "Info") + if loglevel, err := log.ParseLevel(logLevelStr); err == nil { + return loglevel + } else { + log.Warnf("Invalid log level: %v. Log level will be Info!", logLevelStr) + return log.InfoLevel + } + +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/config/config_test.go b/test/usecases/oruclosedlooprecovery/goversion/internal/config/config_test.go new file mode 100644 index 00000000..e278e60c --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/config/config_test.go @@ -0,0 +1,119 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package config + +import ( + "bytes" + "os" + "reflect" + "testing" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +func TestNew_envVarsSetConfigContainSetValues(t *testing.T) { + os.Setenv("LOG_LEVEL", "Debug") + os.Setenv("CONSUMER_HOST", "consumerHost") + os.Setenv("CONSUMER_PORT", "8095") + os.Setenv("INFO_COORD_ADDR", "infoCoordAddr") + os.Setenv("SDNR_HOST", "sdnrHost") + os.Setenv("SDNR_PORT", "3908") + os.Setenv("SDNR_USER", "admin") + os.Setenv("SDNR_PASSWORD", "pwd") + os.Setenv("ORU_TO_ODU_MAP_FILE", "file") + t.Cleanup(func() { + os.Clearenv() + }) + wantConfig := Config{ + LogLevel: log.DebugLevel, + ConsumerHost: "consumerHost", + ConsumerPort: 8095, + InfoCoordinatorAddress: "infoCoordAddr", + SDNRHost: "sdnrHost", + SDNRPort: 3908, + SDNRUser: "admin", + SDNPassword: "pwd", + ORUToODUMapFile: "file", + } + if got := New(); !reflect.DeepEqual(got, &wantConfig) { + t.Errorf("New() = %v, want %v", got, &wantConfig) + } +} + +func TestNew_faultyIntValueSetConfigContainDefaultValueAndWarnInLog(t *testing.T) { + assertions := require.New(t) + var buf bytes.Buffer + log.SetOutput(&buf) + + os.Setenv("CONSUMER_PORT", "wrong") + t.Cleanup(func() { + log.SetOutput(os.Stderr) + os.Clearenv() + }) + wantConfig := Config{ + LogLevel: log.InfoLevel, + ConsumerHost: "", + ConsumerPort: 0, + InfoCoordinatorAddress: "http://enrichmentservice:8083", + SDNRHost: "http://localhost", + SDNRPort: 3904, + SDNRUser: "admin", + SDNPassword: "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U", + ORUToODUMapFile: "o-ru-to-o-du-map.csv", + } + if got := New(); !reflect.DeepEqual(got, &wantConfig) { + t.Errorf("New() = %v, want %v", got, &wantConfig) + } + logString := buf.String() + assertions.Contains(logString, "Invalid int value: wrong for variable: CONSUMER_PORT. Default value: 0 will be used") +} + +func TestNew_envFaultyLogLevelConfigContainDefaultValues(t *testing.T) { + assertions := require.New(t) + var buf bytes.Buffer + log.SetOutput(&buf) + + os.Setenv("LOG_LEVEL", "wrong") + t.Cleanup(func() { + log.SetOutput(os.Stderr) + os.Clearenv() + }) + wantConfig := Config{ + LogLevel: log.InfoLevel, + ConsumerHost: "", + ConsumerPort: 0, + InfoCoordinatorAddress: "http://enrichmentservice:8083", + SDNRHost: "http://localhost", + SDNRPort: 3904, + SDNRUser: "admin", + SDNPassword: "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U", + ORUToODUMapFile: "o-ru-to-o-du-map.csv", + } + if got := New(); !reflect.DeepEqual(got, &wantConfig) { + t.Errorf("New() = %v, want %v", got, &wantConfig) + } + if got := New(); !reflect.DeepEqual(got, &wantConfig) { + t.Errorf("New() = %v, want %v", got, &wantConfig) + } + logString := buf.String() + assertions.Contains(logString, "Invalid log level: wrong. Log level will be Info!") +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/linkfailure/linkfailurehandler.go b/test/usecases/oruclosedlooprecovery/goversion/internal/linkfailure/linkfailurehandler.go new file mode 100644 index 00000000..ebcf3125 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/linkfailure/linkfailurehandler.go @@ -0,0 +1,113 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package linkfailure + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "strings" + + log "github.com/sirupsen/logrus" + + "oransc.org/usecase/oruclosedloop/internal/repository" + "oransc.org/usecase/oruclosedloop/internal/restclient" + "oransc.org/usecase/oruclosedloop/internal/ves" +) + +type Configuration struct { + ConsumerAddress string + InfoCoordAddress string + SDNRAddress string + SDNRUser string + SDNRPassword string +} + +const rawSdnrPath = "/rests/data/network-topology:network-topology/topology=topology-netconf/node=[O-DU-ID]/yang-ext:mount/o-ran-sc-du-hello-world:network-function/du-to-ru-connection=[O-RU-ID]" + +const unlockMessage = `{"o-ran-sc-du-hello-world:du-to-ru-connection": [{"name":"[O-RU-ID]","administrative-state":"UNLOCKED"}]}` + +type LinkFailureHandler struct { + lookupService repository.LookupService + config Configuration +} + +func NewLinkFailureHandler(ls repository.LookupService, conf Configuration) *LinkFailureHandler { + return &LinkFailureHandler{ + lookupService: ls, + config: conf, + } +} + +func (lfh LinkFailureHandler) MessagesHandler(w http.ResponseWriter, r *http.Request) { + log.Debug("Handling messages") + if messages := lfh.getVesMessages(r); messages != nil { + faultMessages := ves.GetFaultMessages(messages) + + for _, message := range faultMessages { + if message.IsLinkFailure() { + lfh.sendUnlockMessage(message.GetORuId()) + } else if message.IsClearLinkFailure() { + log.Debugf("Cleared Link failure for O-RU ID: %v", message.GetORuId()) + } + } + } +} + +func (lfh LinkFailureHandler) sendUnlockMessage(oRuId string) { + if oDuId, err := lfh.lookupService.GetODuID(oRuId); err == nil { + sdnrPath := getSdnrPath(oRuId, oDuId) + unlockMessage := lfh.getUnlockMessage(oRuId) + if error := restclient.Put(lfh.config.SDNRAddress+sdnrPath, unlockMessage, lfh.config.SDNRUser, lfh.config.SDNRPassword); error == nil { + log.Debugf("Sent unlock message for O-RU: %v to O-DU: %v.", oRuId, oDuId) + } else { + log.Warn(error) + } + } else { + log.Warn(err) + } + +} + +func getSdnrPath(oRuId string, oDuId string) string { + sdnrPath := strings.Replace(rawSdnrPath, "[O-DU-ID]", oDuId, 1) + sdnrPath = strings.Replace(sdnrPath, "[O-RU-ID]", oRuId, 1) + return sdnrPath +} + +func (lfh LinkFailureHandler) getUnlockMessage(oRuId string) string { + return strings.Replace(unlockMessage, "[O-RU-ID]", oRuId, 1) +} + +func (lfh LinkFailureHandler) getVesMessages(r *http.Request) *[]string { + var messages []string + body, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Warn(err) + return nil + } + err = json.Unmarshal(body, &messages) + if err != nil { + log.Warn(err) + return nil + } + return &messages +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/linkfailure/linkfailurehandler_test.go b/test/usecases/oruclosedlooprecovery/goversion/internal/linkfailure/linkfailurehandler_test.go new file mode 100644 index 00000000..9653c993 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/linkfailure/linkfailurehandler_test.go @@ -0,0 +1,183 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package linkfailure + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "testing" + + log "github.com/sirupsen/logrus" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "oransc.org/usecase/oruclosedloop/internal/repository" + "oransc.org/usecase/oruclosedloop/internal/restclient" + "oransc.org/usecase/oruclosedloop/internal/ves" + "oransc.org/usecase/oruclosedloop/mocks" +) + +func Test_MessagesHandlerWithLinkFailure(t *testing.T) { + log.SetLevel(log.DebugLevel) + assertions := require.New(t) + + var buf bytes.Buffer + log.SetOutput(&buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + clientMock := mocks.HTTPClient{} + + clientMock.On("Do", mock.Anything).Return(&http.Response{ + StatusCode: http.StatusOK, + }, nil) + + restclient.Client = &clientMock + + lookupServiceMock := mocks.LookupService{} + + lookupServiceMock.On("GetODuID", mock.Anything).Return("HCL-O-DU-1122", nil) + + handlerUnderTest := NewLinkFailureHandler(&lookupServiceMock, Configuration{ + SDNRAddress: "http://localhost:9990", + SDNRUser: "admin", + SDNRPassword: "pwd", + }) + + responseRecorder := httptest.NewRecorder() + r := newRequest(http.MethodPost, "/", getFaultMessage("ERICSSON-O-RU-11220", "CRITICAL"), t) + handler := http.HandlerFunc(handlerUnderTest.MessagesHandler) + handler.ServeHTTP(responseRecorder, r) + assertions.Equal(http.StatusOK, responseRecorder.Result().StatusCode) + + var actualRequest *http.Request + clientMock.AssertCalled(t, "Do", mock.MatchedBy(func(req *http.Request) bool { + actualRequest = req + return true + })) + assertions.Equal(http.MethodPut, actualRequest.Method) + assertions.Equal("http", actualRequest.URL.Scheme) + assertions.Equal("localhost:9990", actualRequest.URL.Host) + expectedSdnrPath := "/rests/data/network-topology:network-topology/topology=topology-netconf/node=HCL-O-DU-1122/yang-ext:mount/o-ran-sc-du-hello-world:network-function/du-to-ru-connection=ERICSSON-O-RU-11220" + assertions.Equal(expectedSdnrPath, actualRequest.URL.Path) + assertions.Equal("application/json; charset=utf-8", actualRequest.Header.Get("Content-Type")) + tempRequest, _ := http.NewRequest("", "", nil) + tempRequest.SetBasicAuth("admin", "pwd") + assertions.Equal(tempRequest.Header.Get("Authorization"), actualRequest.Header.Get("Authorization")) + body, _ := ioutil.ReadAll(actualRequest.Body) + expectedBody := []byte(`{"o-ran-sc-du-hello-world:du-to-ru-connection": [{"name":"ERICSSON-O-RU-11220","administrative-state":"UNLOCKED"}]}`) + assertions.Equal(expectedBody, body) + clientMock.AssertNumberOfCalls(t, "Do", 1) + + logString := buf.String() + assertions.Contains(logString, "Sent unlock message") + assertions.Contains(logString, "O-RU: ERICSSON-O-RU-11220") + assertions.Contains(logString, "O-DU: HCL-O-DU-1122") +} + +func newRequest(method string, url string, bodyAsBytes []byte, t *testing.T) *http.Request { + body := ioutil.NopCloser(bytes.NewReader(bodyAsBytes)) + if req, err := http.NewRequest(method, url, body); err == nil { + return req + } else { + t.Fatalf("Could not create request due to: %v", err) + return nil + } +} + +func Test_MessagesHandlerWithClearLinkFailure(t *testing.T) { + log.SetLevel(log.DebugLevel) + assertions := require.New(t) + + var buf bytes.Buffer + log.SetOutput(&buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + lookupServiceMock := mocks.LookupService{} + + lookupServiceMock.On("GetODuID", mock.Anything).Return("HCL-O-DU-1122", nil) + + handlerUnderTest := NewLinkFailureHandler(&lookupServiceMock, Configuration{}) + + responseRecorder := httptest.NewRecorder() + r := newRequest(http.MethodPost, "/", getFaultMessage("ERICSSON-O-RU-11220", "NORMAL"), t) + handler := http.HandlerFunc(handlerUnderTest.MessagesHandler) + handler.ServeHTTP(responseRecorder, r) + assertions.Equal(http.StatusOK, responseRecorder.Result().StatusCode) + + logString := buf.String() + assertions.Contains(logString, "Cleared Link failure") + assertions.Contains(logString, "O-RU ID: ERICSSON-O-RU-11220") +} + +func Test_MessagesHandlerWithLinkFailureUnmappedORU(t *testing.T) { + log.SetLevel(log.DebugLevel) + assertions := require.New(t) + + var buf bytes.Buffer + log.SetOutput(&buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + lookupServiceMock := mocks.LookupService{} + + lookupServiceMock.On("GetODuID", mock.Anything).Return("", repository.IdNotMappedError{ + Id: "ERICSSON-O-RU-11220", + }) + + handlerUnderTest := NewLinkFailureHandler(&lookupServiceMock, Configuration{}) + + responseRecorder := httptest.NewRecorder() + r := newRequest(http.MethodPost, "/", getFaultMessage("ERICSSON-O-RU-11220", "CRITICAL"), t) + handler := http.HandlerFunc(handlerUnderTest.MessagesHandler) + handler.ServeHTTP(responseRecorder, r) + assertions.Equal(http.StatusOK, responseRecorder.Result().StatusCode) + + logString := buf.String() + assertions.Contains(logString, "O-RU-ID: ERICSSON-O-RU-11220 not mapped.") +} + +func getFaultMessage(sourceName string, eventSeverity string) []byte { + linkFailureMessage := ves.FaultMessage{ + Event: ves.Event{ + CommonEventHeader: ves.CommonEventHeader{ + Domain: "fault", + SourceName: sourceName, + }, + FaultFields: ves.FaultFields{ + AlarmCondition: "28", + EventSeverity: eventSeverity, + }, + }, + } + messageAsByteArray, _ := json.Marshal(linkFailureMessage) + response := [1]string{string(messageAsByteArray)} + responseAsByteArray, _ := json.Marshal(response) + return responseAsByteArray +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/repository/csvhelp.go b/test/usecases/oruclosedlooprecovery/goversion/internal/repository/csvhelp.go new file mode 100644 index 00000000..ccaff8b7 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/repository/csvhelp.go @@ -0,0 +1,51 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package repository + +import ( + "encoding/csv" + "os" +) + +type CsvFileHelper interface { + GetCsvFromFile(name string) ([][]string, error) +} + +type CsvFileHelperImpl struct{} + +func NewCsvFileHelper() CsvFileHelperImpl { + return CsvFileHelperImpl{} +} + +func (h *CsvFileHelperImpl) GetCsvFromFile(name string) ([][]string, error) { + if csvFile, err := os.Open(name); err == nil { + defer csvFile.Close() + reader := csv.NewReader(csvFile) + reader.FieldsPerRecord = -1 + if csvData, err := reader.ReadAll(); err == nil { + return csvData, nil + } else { + return nil, err + } + } else { + return nil, err + } +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/repository/csvhelp_test.go b/test/usecases/oruclosedlooprecovery/goversion/internal/repository/csvhelp_test.go new file mode 100644 index 00000000..dfd29cc9 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/repository/csvhelp_test.go @@ -0,0 +1,82 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package repository + +import ( + "os" + "reflect" + "testing" +) + +func TestCsvFileHelperImpl_GetCsvFromFile(t *testing.T) { + filePath := createTempCsvFile() + defer os.Remove(filePath) + type args struct { + name string + } + tests := []struct { + name string + fileHelper *CsvFileHelperImpl + args args + want [][]string + wantErr bool + }{ + { + name: "Read from file should return array of content", + fileHelper: &CsvFileHelperImpl{}, + args: args{ + name: filePath, + }, + want: [][]string{{"O-RU-ID", "O-DU-ID"}}, + wantErr: false, + }, + { + name: "File missing should return error", + fileHelper: &CsvFileHelperImpl{}, + args: args{ + name: "nofile.csv", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := &CsvFileHelperImpl{} + got, err := h.GetCsvFromFile(tt.args.name) + if (err != nil) != tt.wantErr { + t.Errorf("CsvFileHelperImpl.GetCsvFromFile() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CsvFileHelperImpl.GetCsvFromFile() = %v, want %v", got, tt.want) + } + }) + } +} + +func createTempCsvFile() string { + csvFile, _ := os.CreateTemp("", "test*.csv") + filePath := csvFile.Name() + csvFile.Write([]byte("O-RU-ID,O-DU-ID")) + csvFile.Close() + return filePath +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/repository/lookupservice.go b/test/usecases/oruclosedlooprecovery/goversion/internal/repository/lookupservice.go new file mode 100644 index 00000000..1b6fa693 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/repository/lookupservice.go @@ -0,0 +1,73 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package repository + +import ( + "fmt" +) + +type IdNotMappedError struct { + Id string +} + +func (inme IdNotMappedError) Error() string { + return fmt.Sprintf("O-RU-ID: %v not mapped.", inme.Id) +} + +type LookupService interface { + Init() error + GetODuID(oRuId string) (string, error) +} + +type LookupServiceImpl struct { + csvFileHelper CsvFileHelper + csvFileName string + + oRuIdToODuIdMap map[string]string +} + +func NewLookupServiceImpl(fileHelper CsvFileHelper, fileName string) *LookupServiceImpl { + s := LookupServiceImpl{ + csvFileHelper: fileHelper, + csvFileName: fileName, + } + s.oRuIdToODuIdMap = make(map[string]string) + return &s +} + +func (s LookupServiceImpl) Init() error { + if csvData, err := s.csvFileHelper.GetCsvFromFile(s.csvFileName); err == nil { + for _, each := range csvData { + s.oRuIdToODuIdMap[each[0]] = each[1] + } + return nil + } else { + return err + } +} + +func (s LookupServiceImpl) GetODuID(oRuId string) (string, error) { + if oDuId, ok := s.oRuIdToODuIdMap[oRuId]; ok { + return oDuId, nil + } else { + return "", IdNotMappedError{Id: oRuId} + } +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/repository/lookupservice_test.go b/test/usecases/oruclosedlooprecovery/goversion/internal/repository/lookupservice_test.go new file mode 100644 index 00000000..58194569 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/repository/lookupservice_test.go @@ -0,0 +1,176 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package repository + +import ( + "errors" + "reflect" + "testing" + + "oransc.org/usecase/oruclosedloop/mocks" +) + +func TestNewLookupServiceImpl(t *testing.T) { + mockCsvFileHelper := &mocks.CsvFileHelper{} + type args struct { + fileHelper CsvFileHelper + fileName string + } + tests := []struct { + name string + args args + want *LookupServiceImpl + }{ + { + name: "Should return populated service", + args: args{ + fileHelper: mockCsvFileHelper, + fileName: "test.csv", + }, + want: &LookupServiceImpl{ + csvFileHelper: mockCsvFileHelper, + csvFileName: "test.csv", + oRuIdToODuIdMap: map[string]string{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewLookupServiceImpl(tt.args.fileHelper, tt.args.fileName); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewLookupServiceImpl() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLookupServiceImpl_Init(t *testing.T) { + mockCsvFileHelper := &mocks.CsvFileHelper{} + mockCsvFileHelper.On("GetCsvFromFile", "./map.csv").Return([][]string{{"O-RU-ID", "O-DU-ID"}}, nil).Once() + mockCsvFileHelper.On("GetCsvFromFile", "foo.csv").Return(nil, errors.New("Error")).Once() + type fields struct { + csvFileHelper CsvFileHelper + csvFileName string + oRuIdToODuIdMap map[string]string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "Init with proper csv file should not return error", + fields: fields{ + csvFileHelper: mockCsvFileHelper, + csvFileName: "./map.csv", + oRuIdToODuIdMap: map[string]string{}}, + wantErr: false, + }, + { + name: "Init with missing file should return error", + fields: fields{ + csvFileHelper: mockCsvFileHelper, + csvFileName: "foo.csv", + oRuIdToODuIdMap: map[string]string{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := LookupServiceImpl{ + csvFileHelper: tt.fields.csvFileHelper, + csvFileName: tt.fields.csvFileName, + oRuIdToODuIdMap: tt.fields.oRuIdToODuIdMap, + } + if err := s.Init(); (err != nil) != tt.wantErr { + t.Errorf("LookupServiceImpl.Init() error = %v, wantErr %v", err, tt.wantErr) + } else if !tt.wantErr { + wantedMap := map[string]string{"O-RU-ID": "O-DU-ID"} + if !reflect.DeepEqual(wantedMap, s.oRuIdToODuIdMap) { + t.Errorf("LookupServiceImpl.Init() map not initialized, wanted map: %v, got map: %v", wantedMap, s.oRuIdToODuIdMap) + } + } + }) + } + mockCsvFileHelper.AssertNumberOfCalls(t, "GetCsvFromFile", 2) +} + +func TestLookupServiceImpl_GetODuID(t *testing.T) { + type fields struct { + csvFileHelper CsvFileHelper + csvFileName string + oRuIdToODuIdMap map[string]string + } + type args struct { + oRuId string + } + tests := []struct { + name string + fields fields + args args + want string + wantErr error + }{ + { + name: "Id mapped should return mapped id", + fields: fields{ + csvFileHelper: nil, + csvFileName: "", + oRuIdToODuIdMap: map[string]string{"O-RU-ID": "O-DU-ID"}, + }, + args: args{ + oRuId: "O-RU-ID", + }, + want: "O-DU-ID", + wantErr: nil, + }, + { + name: "Id not mapped should return IdNotMappedError", + fields: fields{ + csvFileHelper: nil, + csvFileName: "", + oRuIdToODuIdMap: map[string]string{}, + }, + args: args{ + oRuId: "O-RU-ID", + }, + want: "", + wantErr: IdNotMappedError{Id: "O-RU-ID"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := LookupServiceImpl{ + csvFileHelper: tt.fields.csvFileHelper, + csvFileName: tt.fields.csvFileName, + oRuIdToODuIdMap: tt.fields.oRuIdToODuIdMap, + } + got, err := s.GetODuID(tt.args.oRuId) + if err != tt.wantErr { + t.Errorf("LookupServiceImpl.GetODuID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("LookupServiceImpl.GetODuID() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/restclient/client.go b/test/usecases/oruclosedlooprecovery/goversion/internal/restclient/client.go new file mode 100644 index 00000000..f038b25d --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/restclient/client.go @@ -0,0 +1,116 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package restclient + +import ( + "bytes" + "fmt" + "io" + "net/http" + "time" +) + +type RequestError struct { + StatusCode int + Body []byte +} + +func (pe RequestError) Error() string { + return fmt.Sprintf("Request failed due to error response with status: %v and body: %v", pe.StatusCode, string(pe.Body)) +} + +// HTTPClient interface +type HTTPClient interface { + Get(url string) (*http.Response, error) + + Do(*http.Request) (*http.Response, error) +} + +var ( + Client HTTPClient +) + +func init() { + Client = &http.Client{ + Timeout: time.Second * 5, + } +} + +func Get(url string) ([]byte, error) { + if response, err := Client.Get(url); err == nil { + defer response.Body.Close() + if responseData, err := io.ReadAll(response.Body); err == nil { + return responseData, nil + } else { + return nil, err + } + } else { + return nil, err + } +} + +func PutWithoutAuth(url string, body []byte) error { + return do(http.MethodPut, url, body) +} + +func Put(url string, body string, userName string, password string) error { + return do(http.MethodPut, url, []byte(body), userName, password) +} + +func Delete(url string) error { + return do(http.MethodDelete, url, nil) +} + +func do(method string, url string, body []byte, userInfo ...string) error { + if req, reqErr := http.NewRequest(method, url, bytes.NewBuffer(body)); reqErr == nil { + if body != nil { + req.Header.Set("Content-Type", "application/json; charset=utf-8") + } + if len(userInfo) > 0 { + req.SetBasicAuth(userInfo[0], userInfo[1]) + } + if response, respErr := Client.Do(req); respErr == nil { + if isResponseSuccess(response.StatusCode) { + return nil + } else { + return getResponseError(response) + } + } else { + return respErr + } + } else { + return reqErr + } +} + +func isResponseSuccess(statusCode int) bool { + return statusCode >= http.StatusOK && statusCode <= 299 +} + +func getResponseError(response *http.Response) RequestError { + defer response.Body.Close() + responseData, _ := io.ReadAll(response.Body) + putError := RequestError{ + StatusCode: response.StatusCode, + Body: responseData, + } + return putError +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/ves/decoder.go b/test/usecases/oruclosedlooprecovery/goversion/internal/ves/decoder.go new file mode 100644 index 00000000..22936125 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/ves/decoder.go @@ -0,0 +1,42 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package ves + +import ( + "encoding/json" + + log "github.com/sirupsen/logrus" +) + +func GetFaultMessages(messageStrings *[]string) []FaultMessage { + faultMessages := make([]FaultMessage, 0, len(*messageStrings)) + for _, msgString := range *messageStrings { + var message FaultMessage + if err := json.Unmarshal([]byte(msgString), &message); err == nil { + if message.isFault() { + faultMessages = append(faultMessages, message) + } + } else { + log.Warn(err) + } + } + return faultMessages +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/ves/decoder_test.go b/test/usecases/oruclosedlooprecovery/goversion/internal/ves/decoder_test.go new file mode 100644 index 00000000..75bee1fb --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/ves/decoder_test.go @@ -0,0 +1,64 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package ves + +import ( + "reflect" + "testing" +) + +func TestGetFaultMessages(t *testing.T) { + type args struct { + messageStrings *[]string + } + tests := []struct { + name string + args args + want []FaultMessage + }{ + { + name: "", + args: args{ + messageStrings: &[]string{"{\"event\":{\"commonEventHeader\":{\"domain\":\"heartbeat\"}}}", + `{"event":{"commonEventHeader":{"domain":"fault","sourceName":"ERICSSON-O-RU-11220"},"faultFields":{"eventSeverity":"CRITICAL","alarmCondition":"28"}}}`}, + }, + want: []FaultMessage{{ + Event: Event{ + CommonEventHeader: CommonEventHeader{ + Domain: "fault", + SourceName: "ERICSSON-O-RU-11220", + }, + FaultFields: FaultFields{ + AlarmCondition: "28", + EventSeverity: "CRITICAL", + }, + }, + }}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetFaultMessages(tt.args.messageStrings); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetFaultMessages() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/ves/message.go b/test/usecases/oruclosedlooprecovery/goversion/internal/ves/message.go new file mode 100644 index 00000000..ca8a3ca8 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/ves/message.go @@ -0,0 +1,64 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package ves + +type FaultMessage struct { + Event Event `json:"event"` +} + +type Event struct { + CommonEventHeader CommonEventHeader `json:"commonEventHeader"` + FaultFields FaultFields `json:"faultFields"` +} + +type CommonEventHeader struct { + Domain string `json:"domain"` + SourceName string `json:"sourceName"` +} + +type FaultFields struct { + AlarmCondition string `json:"alarmCondition"` + EventSeverity string `json:"eventSeverity"` +} + +func (message FaultMessage) isFault() bool { + return message.Event.CommonEventHeader.Domain == "fault" +} + +func (message FaultMessage) isLinkAlarm() bool { + return message.Event.FaultFields.AlarmCondition == "28" +} + +func (message FaultMessage) isSeverityNormal() bool { + return message.Event.FaultFields.EventSeverity == "NORMAL" +} + +func (message FaultMessage) IsLinkFailure() bool { + return message.isFault() && message.isLinkAlarm() && !message.isSeverityNormal() +} + +func (message FaultMessage) IsClearLinkFailure() bool { + return message.isFault() && message.isLinkAlarm() && message.isSeverityNormal() +} + +func (message FaultMessage) GetORuId() string { + return message.Event.CommonEventHeader.SourceName +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/internal/ves/message_test.go b/test/usecases/oruclosedlooprecovery/goversion/internal/ves/message_test.go new file mode 100644 index 00000000..30c7b72c --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/internal/ves/message_test.go @@ -0,0 +1,262 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package ves + +import "testing" + +func TestMessage_isFault(t *testing.T) { + type fields struct { + Event Event + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "is Fault", + fields: fields{ + Event: Event{ + CommonEventHeader: CommonEventHeader{ + Domain: "fault", + }, + }, + }, + want: true, + }, + { + name: "is not Fault", + fields: fields{ + Event: Event{}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := FaultMessage{ + Event: tt.fields.Event, + } + if got := message.isFault(); got != tt.want { + t.Errorf("Message.isFault() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMessage_isLinkAlarm(t *testing.T) { + type fields struct { + Event Event + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "is Link alarm", + fields: fields{ + Event: Event{ + FaultFields: FaultFields{ + AlarmCondition: "28", + }, + }, + }, + want: true, + }, + { + name: "is not Link alarm", + fields: fields{ + Event: Event{ + FaultFields: FaultFields{ + AlarmCondition: "2", + }, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := FaultMessage{ + Event: tt.fields.Event, + } + if got := message.isLinkAlarm(); got != tt.want { + t.Errorf("Message.isLinkAlarm() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMessage_isSeverityNormal(t *testing.T) { + type fields struct { + Event Event + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "is severity NORMAL", + fields: fields{ + Event: Event{ + FaultFields: FaultFields{ + EventSeverity: "NORMAL", + }, + }, + }, + want: true, + }, + { + name: "is not severity NORMAL", + fields: fields{ + Event: Event{ + FaultFields: FaultFields{ + AlarmCondition: "ERROR", + }, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := FaultMessage{ + Event: tt.fields.Event, + } + if got := message.isSeverityNormal(); got != tt.want { + t.Errorf("Message.isSeverityNormal() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMessage_IsLinkFailure(t *testing.T) { + type fields struct { + Event Event + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "is Link Failure", + fields: fields{ + Event: Event{ + CommonEventHeader: CommonEventHeader{ + Domain: "fault", + }, + FaultFields: FaultFields{ + AlarmCondition: "28", + EventSeverity: "ERROR", + }, + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := FaultMessage{ + Event: tt.fields.Event, + } + if got := message.IsLinkFailure(); got != tt.want { + t.Errorf("Message.IsLinkFailure() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMessage_IsClearLinkFailure(t *testing.T) { + type fields struct { + Event Event + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "is not Link Failure", + fields: fields{ + Event: Event{ + CommonEventHeader: CommonEventHeader{ + Domain: "fault", + }, + FaultFields: FaultFields{ + AlarmCondition: "28", + EventSeverity: "NORMAL", + }, + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := FaultMessage{ + Event: tt.fields.Event, + } + if got := message.IsClearLinkFailure(); got != tt.want { + t.Errorf("Message.IsClearLinkFailure() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMessage_GetORuId(t *testing.T) { + type fields struct { + Event Event + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "is not Link Failure", + fields: fields{ + Event: Event{ + CommonEventHeader: CommonEventHeader{ + SourceName: "O-RU-ID", + }, + FaultFields: FaultFields{ + AlarmCondition: "28", + EventSeverity: "NORMAL", + }, + }, + }, + want: "O-RU-ID", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := FaultMessage{ + Event: tt.fields.Event, + } + if got := message.GetORuId(); got != tt.want { + t.Errorf("Message.GetORuId() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/main.go b/test/usecases/oruclosedlooprecovery/goversion/main.go new file mode 100644 index 00000000..5574f8cc --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/main.go @@ -0,0 +1,109 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package main + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" + "oransc.org/usecase/oruclosedloop/internal/config" + "oransc.org/usecase/oruclosedloop/internal/linkfailure" + "oransc.org/usecase/oruclosedloop/internal/repository" + "oransc.org/usecase/oruclosedloop/internal/restclient" +) + +var consumerConfig linkfailure.Configuration +var lookupService repository.LookupService +var host string +var port string + +const jobId = "14e7bb84-a44d-44c1-90b7-6995a92ad43c" + +func init() { + configuration := config.New() + + log.SetLevel(configuration.LogLevel) + + if configuration.ConsumerHost == "" || configuration.ConsumerPort == 0 { + log.Fatal("Consumer host and port must be provided!") + } + host = configuration.ConsumerHost + port = fmt.Sprint(configuration.ConsumerPort) + + csvFileHelper := repository.NewCsvFileHelper() + lookupService = repository.NewLookupServiceImpl(&csvFileHelper, configuration.ORUToODUMapFile) + if initErr := lookupService.Init(); initErr != nil { + log.Fatalf("Unable to create LookupService due to inability to get O-RU-ID to O-DU-ID map. Cause: %v", initErr) + } + consumerConfig = linkfailure.Configuration{ + InfoCoordAddress: configuration.InfoCoordinatorAddress, + SDNRAddress: configuration.SDNRHost + ":" + fmt.Sprint(configuration.SDNRPort), + SDNRUser: configuration.SDNRUser, + SDNRPassword: configuration.SDNPassword, + } +} + +func main() { + defer deleteJob() + messageHandler := linkfailure.NewLinkFailureHandler(lookupService, consumerConfig) + r := mux.NewRouter() + r.HandleFunc("/", messageHandler.MessagesHandler).Methods(http.MethodPost) + r.HandleFunc("/admin/start", startHandler).Methods(http.MethodPost) + r.HandleFunc("/admin/stop", stopHandler).Methods(http.MethodPost) + log.Error(http.ListenAndServe(":"+port, r)) +} + +func startHandler(w http.ResponseWriter, r *http.Request) { + jobRegistrationInfo := struct { + InfoTypeId string `json:"info_type_id"` + JobResultUri string `json:"job_result_uri"` + JobOwner string `json:"job_owner"` + JobDefinition interface{} `json:"job_definition"` + }{ + InfoTypeId: "STD_Fault_Messages", + JobResultUri: host + ":" + port, + JobOwner: "O-RU Closed Loop Usecase", + JobDefinition: "{}", + } + body, _ := json.Marshal(jobRegistrationInfo) + putErr := restclient.PutWithoutAuth(consumerConfig.InfoCoordAddress+"/data-consumer/v1/info-jobs/"+jobId, body) + if putErr != nil { + http.Error(w, fmt.Sprintf("Unable to register consumer job: %v", putErr), http.StatusBadRequest) + return + } + log.Debug("Registered job.") +} + +func stopHandler(w http.ResponseWriter, r *http.Request) { + deleteErr := deleteJob() + if deleteErr != nil { + http.Error(w, fmt.Sprintf("Unable to delete consumer job: %v", deleteErr), http.StatusBadRequest) + return + } + log.Debug("Deleted job.") +} + +func deleteJob() error { + return restclient.Delete(consumerConfig.InfoCoordAddress + "/data-consumer/v1/info-jobs/" + jobId) +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/mocks/CsvFileHelper.go b/test/usecases/oruclosedlooprecovery/goversion/mocks/CsvFileHelper.go new file mode 100644 index 00000000..b857de57 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/mocks/CsvFileHelper.go @@ -0,0 +1,33 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// CsvFileHelper is an autogenerated mock type for the CsvFileHelper type +type CsvFileHelper struct { + mock.Mock +} + +// GetCsvFromFile provides a mock function with given fields: name +func (_m *CsvFileHelper) GetCsvFromFile(name string) ([][]string, error) { + ret := _m.Called(name) + + var r0 [][]string + if rf, ok := ret.Get(0).(func(string) [][]string); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([][]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/mocks/HTTPClient.go b/test/usecases/oruclosedlooprecovery/goversion/mocks/HTTPClient.go new file mode 100644 index 00000000..3037798b --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/mocks/HTTPClient.go @@ -0,0 +1,60 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + http "net/http" + + mock "github.com/stretchr/testify/mock" +) + +// HTTPClient is an autogenerated mock type for the HTTPClient type +type HTTPClient struct { + mock.Mock +} + +// Do provides a mock function with given fields: _a0 +func (_m *HTTPClient) Do(_a0 *http.Request) (*http.Response, error) { + ret := _m.Called(_a0) + + var r0 *http.Response + if rf, ok := ret.Get(0).(func(*http.Request) *http.Response); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*http.Response) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*http.Request) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Get provides a mock function with given fields: url +func (_m *HTTPClient) Get(url string) (*http.Response, error) { + ret := _m.Called(url) + + var r0 *http.Response + if rf, ok := ret.Get(0).(func(string) *http.Response); ok { + r0 = rf(url) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*http.Response) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(url) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/mocks/LookupService.go b/test/usecases/oruclosedlooprecovery/goversion/mocks/LookupService.go new file mode 100644 index 00000000..2ba83696 --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/mocks/LookupService.go @@ -0,0 +1,45 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// LookupService is an autogenerated mock type for the LookupService type +type LookupService struct { + mock.Mock +} + +// GetODuID provides a mock function with given fields: oRuId +func (_m *LookupService) GetODuID(oRuId string) (string, error) { + ret := _m.Called(oRuId) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(oRuId) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(oRuId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Init provides a mock function with given fields: +func (_m *LookupService) Init() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/test/usecases/oruclosedlooprecovery/goversion/o-ru-to-o-du-map.csv b/test/usecases/oruclosedlooprecovery/goversion/o-ru-to-o-du-map.csv new file mode 100644 index 00000000..951337af --- /dev/null +++ b/test/usecases/oruclosedlooprecovery/goversion/o-ru-to-o-du-map.csv @@ -0,0 +1,11 @@ +ERICSSON-O-RU-11220,HCL-O-DU-1122 +ERICSSON-O-RU-11221,HCL-O-DU-1122 +ERICSSON-O-RU-11222,HCL-O-DU-1122 +ERICSSON-O-RU-11223,HCL-O-DU-1122 +ERICSSON-O-RU-11223,HCL-O-DU-1122 +ERICSSON-O-RU-11224,HCL-O-DU-1123 +ERICSSON-O-RU-11225,HCL-O-DU-1123 +ERICSSON-O-RU-11226,HCL-O-DU-1123 +ERICSSON-O-RU-11227,HCL-O-DU-1124 +ERICSSON-O-RU-11228,HCL-O-DU-1125 +ERICSSON-O-RU-11229,HCL-O-DU-1125 \ No newline at end of file -- 2.16.6