--- /dev/null
+*.out
+.history
+
+oruclosedloop
--- /dev/null
+##
+## 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"]
--- /dev/null
+#!/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"
--- /dev/null
+# 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
--- /dev/null
+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
+)
--- /dev/null
+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=
--- /dev/null
+// -
+// ========================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
+ }
+
+}
--- /dev/null
+// -
+// ========================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!")
+}
--- /dev/null
+// -
+// ========================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
+}
--- /dev/null
+// -
+// ========================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
+}
--- /dev/null
+// -
+// ========================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
+ }
+}
--- /dev/null
+// -
+// ========================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
+}
--- /dev/null
+// -
+// ========================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}
+ }
+}
--- /dev/null
+// -
+// ========================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)
+ }
+ })
+ }
+}
--- /dev/null
+// -
+// ========================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
+}
--- /dev/null
+// -
+// ========================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
+}
--- /dev/null
+// -
+// ========================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)
+ }
+ })
+ }
+}
--- /dev/null
+// -
+// ========================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
+}
--- /dev/null
+// -
+// ========================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)
+ }
+ })
+ }
+}
--- /dev/null
+// -
+// ========================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)
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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
+}
--- /dev/null
+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