From: ychacon Date: Mon, 7 Mar 2022 16:04:12 +0000 (+0100) Subject: Consumer O-DU slice assurance rApp X-Git-Tag: 1.1.0~7 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?p=nonrtric%2Frapp%2Fransliceassurance.git;a=commitdiff_plain;h=16f26ec9bec7e406398340473de228706f75e97f Consumer O-DU slice assurance rApp Issue-ID: NONRTRIC-731 Signed-off-by: ychacon Change-Id: Iade55bfb41f9cd642b477aea7d9e523c39c2e626 --- diff --git a/icsversion/go.mod b/icsversion/go.mod new file mode 100644 index 0000000..38d2c48 --- /dev/null +++ b/icsversion/go.mod @@ -0,0 +1,10 @@ +module oransc.org/usecase/oduclosedloop/icsversion + +go 1.17 + +require ( + github.com/gorilla/mux v1.8.0 + github.com/sirupsen/logrus v1.8.1 +) + +require golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect diff --git a/icsversion/go.sum b/icsversion/go.sum new file mode 100644 index 0000000..24777da --- /dev/null +++ b/icsversion/go.sum @@ -0,0 +1,12 @@ +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/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/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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= diff --git a/icsversion/internal/config/config.go b/icsversion/internal/config/config.go new file mode 100644 index 0000000..fcf2721 --- /dev/null +++ b/icsversion/internal/config/config.go @@ -0,0 +1,85 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 ( + "fmt" + "os" + "strconv" + + log "github.com/sirupsen/logrus" +) + +type Configuration struct { + ConsumerHost string + ConsumerPort int + SDNRAddress string + SDNRUser string + SDNPassword string + InfoCoordinatorAddress string + LogLevel log.Level +} + +func New() *Configuration { + return &Configuration{ + ConsumerHost: getEnv("CONSUMER_HOST", ""), + ConsumerPort: getEnvAsInt("CONSUMER_PORT", 0), + SDNRAddress: getEnv("SDNR_ADDR", "http://localhost:3904"), + SDNRUser: getEnv("SDNR_USER", "admin"), + SDNPassword: getEnv("SDNR_PASSWORD", "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U"), + InfoCoordinatorAddress: getEnv("INFO_COORD_ADDR", "http://enrichmentservice:8083"), + LogLevel: getLogLevel(), + } +} + +func (c Configuration) String() string { + return fmt.Sprintf("[ConsumerHost: %v, ConsumerPort: %v, SDNRAddress: %v, SDNRUser: %v, SDNRPassword: %v, InfoCoordinatorAddress: %v, LogLevel: %v]", c.ConsumerHost, c.ConsumerPort, c.SDNRAddress, c.SDNRUser, c.SDNPassword, c.InfoCoordinatorAddress, c.LogLevel) +} + +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/icsversion/internal/config/config_test.go b/icsversion/internal/config/config_test.go new file mode 100644 index 0000000..fffc60b --- /dev/null +++ b/icsversion/internal/config/config_test.go @@ -0,0 +1,121 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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" + "testing" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +func TestConfigurationValuesSetProperly(t *testing.T) { + + assertions := require.New(t) + + type args struct { + conf Configuration + envVar map[string]string + } + tests := []struct { + name string + args args + }{ + { + name: "Test env variable contain correct set values", + args: args{ + conf: Configuration{ + ConsumerHost: "consumerHost", + ConsumerPort: 8095, + SDNRAddress: "sdnrAddr", + SDNRUser: "admin", + SDNPassword: "pass", + InfoCoordinatorAddress: "infoCoordAddr", + LogLevel: log.InfoLevel, + }, + }, + }, + { + name: "Test faulty int value is set for consumer port variable", + args: args{ + conf: Configuration{ + ConsumerHost: "consumerHost", + SDNRAddress: "sdnrAddr", + SDNRUser: "admin", + SDNPassword: "pass", + InfoCoordinatorAddress: "infoCoordAddr", + LogLevel: log.InfoLevel, + }, + envVar: map[string]string{"CONSUMER_PORT": "wrong"}, + }, + }, + { + name: "Test log level is wrongly set", + args: args{ + conf: Configuration{ + ConsumerHost: "consumerHost", + ConsumerPort: 8095, + SDNRAddress: "sdnrAddr", + SDNRUser: "admin", + SDNPassword: "pass", + InfoCoordinatorAddress: "infoCoordAddr", + LogLevel: log.InfoLevel, + }, + envVar: map[string]string{"LOG_LEVEL": "wrong"}, + }, + }, + } + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("CONSUMER_HOST", "consumerHost") + os.Setenv("CONSUMER_PORT", "8095") + os.Setenv("SDNR_ADDR", "sdnrAddr") + os.Setenv("SDNR_USER", "admin") + os.Setenv("SDNR_PASSWORD", "pass") + os.Setenv("INFO_COORD_ADDR", "infoCoordAddr") + + for key, element := range tt.args.envVar { + os.Setenv(key, element) + } + + var buf bytes.Buffer + log.SetOutput(&buf) + t.Cleanup(func() { + log.SetOutput(os.Stderr) + os.Clearenv() + }) + + got := New() + assertions.Equal(&tt.args.conf, got) + + logString := buf.String() + if i == 1 { + assertions.Contains(logString, "Invalid int value: wrong for variable: CONSUMER_PORT. Default value: 0 will be used") + } + if i == 2 { + assertions.Contains(logString, "Invalid log level: wrong. Log level will be Info!") + } + }) + } +} diff --git a/icsversion/internal/odusliceassurance/app.go b/icsversion/internal/odusliceassurance/app.go new file mode 100644 index 0000000..d313192 --- /dev/null +++ b/icsversion/internal/odusliceassurance/app.go @@ -0,0 +1,156 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 sliceassurance + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" + "oransc.org/usecase/oduclosedloop/icsversion/internal/config" + "oransc.org/usecase/oduclosedloop/icsversion/internal/restclient" + "oransc.org/usecase/oduclosedloop/icsversion/internal/structures" +) + +var started bool +var icsAddr string +var consumerPort string + +const ( + THRESHOLD_TPUT = 7000 + DEFAULT_DEDICATED_RATIO = 15 + NEW_DEDICATED_RATIO = 25 + NODE_ID = "O-DU-1122" + jobId = "14e7bb84-a44d-44c1-90b7-6995a92ad83d" +) + +type App struct { + client *restclient.Client + data *structures.SliceAssuranceMeas + sdnr SdnrHandler + mh MessageHandler +} + +var sdnrConfig SdnrConfiguration + +func (a *App) Initialize(config *config.Configuration) { + consumerPort = fmt.Sprint(config.ConsumerPort) + + sdnrConfig = SdnrConfiguration{ + SDNRAddress: config.SDNRAddress, + SDNRUser: config.SDNRUser, + SDNRPassword: config.SDNPassword, + } + icsAddr = config.InfoCoordinatorAddress + + a.client = restclient.New(&http.Client{}, false) + a.data = structures.NewSliceAssuranceMeas() + + a.sdnr = *NewSdnrHandler(sdnrConfig, a.client, a.data) + a.mh = *NewMessageHandler(a.data) +} + +func (a *App) StartServer() { + fmt.Printf("Starting Server %v", consumerPort) + err := http.ListenAndServe(fmt.Sprintf(":%v", consumerPort), a.getRouter()) + + if err != nil { + log.Errorf("Server stopped unintentionally due to: %v. Deleting job.", err) + if deleteErr := a.deleteJob(); deleteErr != nil { + log.Errorf("Unable to delete consumer job due to: %v. Please remove job %v manually.", deleteErr, jobId) + } + } +} + +func (a *App) getRouter() *mux.Router { + + r := mux.NewRouter() + r.HandleFunc("/", a.run).Methods(http.MethodPost).Name("messageHandler") + r.HandleFunc("/status", a.statusHandler).Methods(http.MethodGet).Name("status") + r.HandleFunc("/admin/start", a.startHandler).Methods(http.MethodPost).Name("start") + r.HandleFunc("/admin/stop", a.stopHandler).Methods(http.MethodPost).Name("stop") + + return r +} + +func (a *App) run(w http.ResponseWriter, r *http.Request) { + a.mh.ProcessMessage(r.Body) + + for key := range a.data.Metrics { + a.sdnr.getRRMInformation(key.Duid) + } + a.sdnr.updateDedicatedRatio() +} + +func (a *App) statusHandler(w http.ResponseWriter, r *http.Request) { + log.Debug("statusHandler:") + runStatus := "started" + if !started { + runStatus = "stopped" + } + fmt.Fprintf(w, `{"status": "%v"}`, runStatus) +} + +func (a *App) startHandler(w http.ResponseWriter, r *http.Request) { + log.Debug("startHandler: Register job in ICS.") + 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: "Performance_Measurement_Streaming", + JobResultURI: "", + JobOwner: "O-DU Slice Assurance Usecase", + JobDefinition: "{}", + } + putErr := a.client.Put(icsAddr+"/data-consumer/v1/info-jobs/"+jobId, jobRegistrationInfo, nil) + if putErr != nil { + http.Error(w, fmt.Sprintf("Unable to register consumer job due to: %v.", putErr), http.StatusBadRequest) + return + } + log.Debug("Registered job.") + started = true +} + +func (a *App) stopHandler(w http.ResponseWriter, r *http.Request) { + deleteErr := a.deleteJob() + if deleteErr != nil { + http.Error(w, fmt.Sprintf("Unable to delete consumer job due to: %v. Please remove job %v manually.", deleteErr, jobId), http.StatusBadRequest) + return + } + log.Debug("Deleted job.") + started = false +} + +func (a *App) deleteJob() error { + return a.client.Delete(icsAddr+"/data-consumer/v1/info-jobs/"+jobId, nil, nil) +} + +func (a *App) Teardown() { + log.Info("Shutting down gracefully.") + deleteErr := a.deleteJob() + if deleteErr != nil { + log.Errorf("Unable to delete job on shutdown due to: %v. Please remove job %v manually.", deleteErr, jobId) + } + +} diff --git a/icsversion/internal/odusliceassurance/messageHandler.go b/icsversion/internal/odusliceassurance/messageHandler.go new file mode 100644 index 0000000..2aee075 --- /dev/null +++ b/icsversion/internal/odusliceassurance/messageHandler.go @@ -0,0 +1,88 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 sliceassurance + +import ( + "encoding/json" + "io" + "io/ioutil" + + log "github.com/sirupsen/logrus" + "oransc.org/usecase/oduclosedloop/icsversion/internal/structures" + "oransc.org/usecase/oduclosedloop/icsversion/messages" +) + +type MessageHandler struct { + data *structures.SliceAssuranceMeas +} + +func NewMessageHandler(data *structures.SliceAssuranceMeas) *MessageHandler { + return &MessageHandler{ + data: data, + } +} + +func (handler MessageHandler) ProcessMessage(body io.ReadCloser) { + log.Debug("Process messages from Dmaap mediator") + + if messages := getVesMessages(body); messages != nil { + stdMessages := getStdMessages(messages) + + for _, message := range stdMessages { + for _, meas := range message.GetMeasurements() { + log.Infof("Create sliceMetric and check if metric exist and update existing one or create new one measurement: %+v\n", meas) + //Create sliceMetric and check if metric exist and update existing one or create new one + if _, err := handler.data.AddOrUpdateMetric(meas); err != nil { + log.Error("Metric could not be added ", err) + } + } + } + } + +} + +func getVesMessages(r io.ReadCloser) *[]string { + var messages []string + body, err := ioutil.ReadAll(r) + if err != nil { + log.Warn(err) + return nil + } + err = json.Unmarshal(body, &messages) + if err != nil { + log.Warn(err) + return nil + } + return &messages +} + +func getStdMessages(messageStrings *[]string) []messages.StdDefinedMessage { + stdMessages := make([]messages.StdDefinedMessage, 0, len(*messageStrings)) + for _, msgString := range *messageStrings { + var message messages.StdDefinedMessage + if err := json.Unmarshal([]byte(msgString), &message); err == nil { + stdMessages = append(stdMessages, message) + } else { + log.Warn(err) + } + } + return stdMessages +} diff --git a/icsversion/internal/odusliceassurance/messageHandler_test.go b/icsversion/internal/odusliceassurance/messageHandler_test.go new file mode 100644 index 0000000..4602eaa --- /dev/null +++ b/icsversion/internal/odusliceassurance/messageHandler_test.go @@ -0,0 +1,89 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 sliceassurance + +import ( + "reflect" + "testing" + + "oransc.org/usecase/oduclosedloop/icsversion/messages" +) + +func TestGetStdDefinedMessages(t *testing.T) { + type args struct { + messageStrings *[]string + } + tests := []struct { + name string + args args + want []messages.StdDefinedMessage + }{ + { + name: "", + args: args{ + messageStrings: &[]string{ + `{"event":{"commonEventHeader":{"domain":"stnd","eventId":"pm-1","eventName":"stndDefined","eventType":"performanceMeasurementStreaming","sequence":825,"priority":"Low","reportingEntityId":"","reportingEntityName":"O-DU-1122","sourceId":"","sourceName":"O-DU-1122","stndDefinedNamespace":"o-ran-sc-du-hello-world","version":"4.1","vesEventListenerVersion":"7.2.1"},"stndDefinedFields":{"stndDefinedFieldsVersion":"1.0","schemaReference":"o-ran-sc-du-hello-world","data":{"id":"pm-1_123","administrative-state":"unlocked","operational-state":"enabled","user-label":"pm","job-tag":"my-job-tag","granularity-period":30,"measurements":[{"measurement-type-instance-reference":"reference","value":5861,"unit":"kbit/s"}]}}}}`, + }, + }, + want: []messages.StdDefinedMessage{{ + Event: messages.Event{ + CommonEventHeader: messages.CommonEventHeader{ + Domain: "stnd", + EventId: "pm-1", + EventName: "stndDefined", + EventType: "performanceMeasurementStreaming", + Sequence: 825, + Priority: "Low", + ReportingEntityName: "O-DU-1122", + SourceName: "O-DU-1122", + StndDefinedNamespace: "o-ran-sc-du-hello-world", + Version: "4.1", + VesEventListenerVersion: "7.2.1", + }, + StndDefinedFields: messages.StndDefinedFields{ + StndDefinedFieldsVersion: "1.0", + SchemaReference: "o-ran-sc-du-hello-world", + Data: messages.Data{ + DataId: "pm-1_123", + AdministrativeState: "unlocked", + OperationalState: "enabled", + UserLabel: "pm", + JobTag: "my-job-tag", + GranularityPeriod: 30, + Measurements: []messages.Measurement{{ + MeasurementTypeInstanceReference: "reference", + Value: 5861, + Unit: "kbit/s", + }}, + }, + }, + }, + }}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getStdMessages(tt.args.messageStrings); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getStdMessages() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/icsversion/internal/odusliceassurance/sdnrHandler.go b/icsversion/internal/odusliceassurance/sdnrHandler.go new file mode 100644 index 0000000..d2e2d87 --- /dev/null +++ b/icsversion/internal/odusliceassurance/sdnrHandler.go @@ -0,0 +1,103 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 sliceassurance + +import ( + "encoding/json" + "fmt" + + log "github.com/sirupsen/logrus" + "oransc.org/usecase/oduclosedloop/icsversion/internal/restclient" + "oransc.org/usecase/oduclosedloop/icsversion/internal/structures" + "oransc.org/usecase/oduclosedloop/icsversion/messages" +) + +type SdnrConfiguration struct { + SDNRAddress string + SDNRUser string + SDNRPassword string +} + +type SdnrHandler struct { + config SdnrConfiguration + client *restclient.Client + data *structures.SliceAssuranceMeas +} + +func NewSdnrHandler(conf SdnrConfiguration, client *restclient.Client, data *structures.SliceAssuranceMeas) *SdnrHandler { + return &SdnrHandler{ + config: conf, + client: client, + data: data, + } +} + +func (handler SdnrHandler) getRRMInformation(duid string) { + var duRRMPolicyRatio messages.ORanDuRestConf + + log.Infof("Get RRM Information from SDNR url: %v", handler.config.SDNRAddress) + if error := handler.client.Get(getUrlForDistributedUnitFunctions(handler.config.SDNRAddress, duid), &duRRMPolicyRatio, handler.config.SDNRUser, handler.config.SDNRPassword); error == nil { + prettyPrint(duRRMPolicyRatio.DistributedUnitFunction) + } else { + log.Warn("Send of Get RRM Information failed! ", error) + } + + for _, odu := range duRRMPolicyRatio.DistributedUnitFunction { + for _, policy := range odu.RRMPolicyRatio { + log.Infof("Add or Update policy: %+v from DU id: %v", policy.Id, duid) + handler.data.AddNewPolicy(duid, policy) + } + } +} + +func (handler SdnrHandler) updateDedicatedRatio() { + for _, metric := range handler.data.Metrics { + policy, check := handler.data.Policies[metric.RRMPolicyRatioId] + //TODO What happened if dedicated ratio is already higher that default and threshold is exceed? + if check && policy.PolicyDedicatedRatio <= DEFAULT_DEDICATED_RATIO { + log.Infof("Send Request to update DedicatedRatio for DU id: %v Policy id: %v", metric.DUId, policy.PolicyRatioId) + path := getUrlUpdatePolicyDedicatedRatio(handler.config.SDNRAddress, metric.DUId, policy.PolicyRatioId) + updatePolicyMessage := policy.GetUpdateDedicatedRatioMessage(metric.SliceDiff, metric.SliceServiceType, NEW_DEDICATED_RATIO) + prettyPrint(updatePolicyMessage) + if error := handler.client.Put(path, updatePolicyMessage, nil, handler.config.SDNRUser, handler.config.SDNRPassword); error == nil { + log.Infof("Policy Dedicated Ratio for PolicyId: %v was updated to %v", policy.PolicyRatioId, NEW_DEDICATED_RATIO) + } else { + log.Warn("Send of Put Request to update DedicatedRatio failed! ", error) + } + } + } +} + +func getUrlForDistributedUnitFunctions(host string, duid string) string { + return host + "/rests/data/network-topology:network-topology/topology=topology-netconf/node=" + NODE_ID + "/yang-ext:mount/o-ran-sc-du-hello-world:network-function/distributed-unit-functions=" + duid +} + +func getUrlUpdatePolicyDedicatedRatio(host string, duid string, policyid string) string { + return host + "/rests/data/network-topology:network-topology/topology=topology-netconf/node=" + NODE_ID + "/yang-ext:mount/o-ran-sc-du-hello-world:network-function/distributed-unit-functions=" + duid + "/radio-resource-management-policy-ratio=" + policyid +} + +func prettyPrint(jsonStruct interface{}) { + b, err := json.MarshalIndent(jsonStruct, "", " ") + if err != nil { + fmt.Println("error:", err) + } + fmt.Print(string(b)) +} diff --git a/icsversion/internal/restclient/client.go b/icsversion/internal/restclient/client.go new file mode 100644 index 0000000..081ebed --- /dev/null +++ b/icsversion/internal/restclient/client.go @@ -0,0 +1,223 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httputil" + + log "github.com/sirupsen/logrus" +) + +type RequestError struct { + StatusCode int + Body []byte +} + +func (e RequestError) Error() string { + return fmt.Sprintf("error response with status: %v and body: %v", e.StatusCode, string(e.Body)) +} + +type Client struct { + httpClient *http.Client + verbose bool +} + +func New(httpClient *http.Client, verbose bool) *Client { + return &Client{ + httpClient: httpClient, + verbose: verbose, + } +} + +func (c *Client) Get(path string, v interface{}, userInfo ...string) error { + var req *http.Request + var err error + + if len(userInfo) > 1 { + req, err = c.newRequest(http.MethodGet, path, nil, userInfo[0], userInfo[1]) + } else { + req, err = c.newRequest(http.MethodGet, path, nil) + } + + if err != nil { + return fmt.Errorf("failed to create GET request: %w", err) + } + + if err := c.doRequest(req, v); err != nil { + return err + } + + return nil +} + +func (c *Client) Post(path string, payload interface{}, v interface{}, userInfo ...string) error { + var req *http.Request + var err error + + if len(userInfo) > 1 { + req, err = c.newRequest(http.MethodPost, path, payload, userInfo[0], userInfo[1]) + } else { + req, err = c.newRequest(http.MethodPost, path, payload) + } + + if err != nil { + return fmt.Errorf("failed to create POST request: %w", err) + } + + if err := c.doRequest(req, v); err != nil { + return err + } + + return nil +} + +func (c *Client) Put(path string, payload interface{}, v interface{}, userInfo ...string) error { + var req *http.Request + var err error + if len(userInfo) > 1 { + req, err = c.newRequest(http.MethodPut, path, payload, userInfo[0], userInfo[1]) + } else { + req, err = c.newRequest(http.MethodPut, path, payload) + } + + if err != nil { + return fmt.Errorf("failed to create PUT request: %w", err) + } + + if err := c.doRequest(req, v); err != nil { + return err + } + + return nil +} + +func (c *Client) Delete(path string, payload interface{}, v interface{}, userInfo ...string) error { + var req *http.Request + var err error + if len(userInfo) > 1 { + req, err = c.newRequest(http.MethodDelete, path, payload, userInfo[0], userInfo[1]) + } else { + req, err = c.newRequest(http.MethodDelete, path, payload) + } + + if err != nil { + return fmt.Errorf("failed to create Delete request: %w", err) + } + + if err := c.doRequest(req, v); err != nil { + return err + } + + return nil +} + +func (c *Client) newRequest(method, path string, payload interface{}, userInfo ...string) (*http.Request, error) { + var reqBody io.Reader + + if payload != nil { + bodyBytes, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + reqBody = bytes.NewReader(bodyBytes) + } + + req, err := http.NewRequest(method, path, reqBody) + + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request: %w", err) + } + + if len(userInfo) > 0 { + req.SetBasicAuth(userInfo[0], userInfo[1]) + } + + if reqBody != nil { + req.Header.Set("Content-Type", "application/json") + } + + if c.verbose { + if reqDump, error := httputil.DumpRequest(req, true); error != nil { + fmt.Println(err) + } else { + fmt.Println(string(reqDump)) + } + } + + return req, nil +} + +func (c *Client) doRequest(r *http.Request, v interface{}) error { + resp, err := c.do(r) + if err != nil { + return err + } + + if resp == nil { + return nil + } + defer resp.Body.Close() + + if v == nil { + return nil + } + + dec := json.NewDecoder(resp.Body) + if err := dec.Decode(&v); err != nil { + return fmt.Errorf("could not parse response body: %w [%s:%s]", err, r.Method, r.URL.String()) + } + log.Debugf("Http Client Response: %v\n", v) + return nil +} + +func (c *Client) do(r *http.Request) (*http.Response, error) { + resp, err := c.httpClient.Do(r) + if err != nil { + return nil, fmt.Errorf("failed to make request [%s:%s]: %w", r.Method, r.URL.String(), err) + } + + if c.verbose { + if responseDump, error := httputil.DumpResponse(resp, true); error != nil { + fmt.Println(err) + } else { + fmt.Println(string(responseDump)) + } + } + + if resp.StatusCode >= http.StatusOK && resp.StatusCode <= 299 { + return resp, nil + } + + defer resp.Body.Close() + responseData, _ := io.ReadAll(resp.Body) + + putError := RequestError{ + StatusCode: resp.StatusCode, + Body: responseData, + } + + return resp, putError +} diff --git a/icsversion/internal/restclient/client_test.go b/icsversion/internal/restclient/client_test.go new file mode 100644 index 0000000..e3dc452 --- /dev/null +++ b/icsversion/internal/restclient/client_test.go @@ -0,0 +1,385 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewRequest(t *testing.T) { + assertions := require.New(t) + + bodyBytes, _ := json.Marshal("body") + succesfullReq, _ := http.NewRequest(http.MethodGet, "url", bytes.NewReader(bodyBytes)) + + type args struct { + method string + path string + payload interface{} + userInfo [2]string + } + tests := []struct { + name string + args args + want *http.Request + wantErr error + }{ + { + name: "succesfull newRequest", + args: args{ + method: http.MethodGet, + path: "url", + payload: "body", + userInfo: [2]string{"user", "pass"}, + }, + want: succesfullReq, + wantErr: nil, + }, + { + name: "request failed json marshal", + args: args{ + method: http.MethodGet, + path: "url", + payload: map[string]interface{}{ + "foo": make(chan int), + }, + }, + want: nil, + wantErr: fmt.Errorf("failed to marshal request body: json: unsupported type: chan int"), + }, + { + name: "request failed calling newRequest", + args: args{ + method: "*?", + path: "url", + payload: "body", + }, + want: nil, + wantErr: fmt.Errorf("failed to create HTTP request: net/http: invalid method \"*?\""), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := New(&http.Client{}, false) + var req *http.Request + var err error + if tt.args.userInfo[0] != "" { + req, err = client.newRequest(tt.args.method, tt.args.path, tt.args.payload, tt.args.userInfo[0], tt.args.userInfo[1]) + } else { + req, err = client.newRequest(tt.args.method, tt.args.path, tt.args.payload) + } + + if tt.wantErr != nil { + assertions.Equal(tt.want, req) + assertions.EqualError(tt.wantErr, err.Error()) + } else { + assertions.Equal("url", req.URL.Path) + assertions.Equal("application/json", req.Header.Get("Content-Type")) + if tt.args.userInfo[0] != "" { + assertions.Contains(req.Header.Get("Authorization"), "Basic dXNlcjpwYXNz") + } else { + assertions.Empty(req.Header.Get("Authorization")) + } + + assertions.Nil(err) + } + + }) + } +} + +func TestGet(t *testing.T) { + assertions := require.New(t) + type args struct { + header string + respCode int + resp interface{} + userInfo [2]string + } + tests := []struct { + name string + args args + wantErr string + }{ + { + name: "successful GET request", + args: args{ + header: "application/json", + respCode: http.StatusOK, + resp: "Success!", + userInfo: [2]string{"user", "pass"}, + }, + wantErr: "", + }, + { + name: "error GET request", + args: args{ + header: "application/json", + respCode: http.StatusBadRequest, + resp: nil, + }, + wantErr: "error response with status: 400 and body:", + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertions.Equal(http.MethodGet, r.Method) + response, _ := json.Marshal(tt.args.resp) + w.Header().Set("Content-Type", tt.args.header) + w.WriteHeader(tt.args.respCode) + w.Write(response) + })) + defer srv.Close() + + client := New(&http.Client{}, false) + var err error + var res interface{} + if tt.args.userInfo[0] != "" { + err = client.Get(srv.URL, &res, tt.args.userInfo[0], tt.args.userInfo[1]) + } else { + err = client.Get(srv.URL, &res) + } + + if err != nil { + assertions.Contains(err.Error(), tt.wantErr) + } + assertions.Equal(tt.args.resp, res) + }) + } +} + +func TestPost(t *testing.T) { + assertions := require.New(t) + + type args struct { + header string + payload interface{} + respCode int + resp interface{} + userInfo [2]string + } + tests := []struct { + name string + args args + wantErr string + }{ + { + name: "successful POST request", + args: args{ + header: "application/json", + payload: `json:"example"`, + respCode: http.StatusOK, + resp: "Success!", + userInfo: [2]string{"user", "pass"}, + }, + wantErr: "", + }, + { + name: "error POST request", + args: args{ + header: "application/json", + payload: `json:"example"`, + respCode: http.StatusBadRequest, + resp: nil, + }, + wantErr: "error response with status: 400 and body:", + }, + { + name: "error to create POST request", + args: args{ + header: "application/json", + payload: map[string]interface{}{ + "foo": make(chan int), + }, + respCode: http.StatusBadRequest, + resp: nil, + }, + wantErr: "failed to marshal request body: json: unsupported type: chan int", + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + assert.Equal(t, http.MethodPost, r.Method) + assert.Contains(t, r.Header.Get("Content-Type"), "application/json") + + var reqBody string + decoder := json.NewDecoder(r.Body) + decoder.Decode(&reqBody) + assert.Equal(t, reqBody, `json:"example"`) + + response, _ := json.Marshal(tt.args.resp) + w.Header().Set("Content-Type", tt.args.header) + w.WriteHeader(tt.args.respCode) + w.Write(response) + })) + defer srv.Close() + + client := New(&http.Client{}, false) + var err error + if tt.args.userInfo[0] != "" { + err = client.Post(srv.URL, tt.args.payload, nil, tt.args.userInfo[0], tt.args.userInfo[1]) + } else { + err = client.Post(srv.URL, tt.args.payload, nil) + } + + if err != nil { + assertions.Contains(err.Error(), tt.wantErr) + } else { + assertions.Equal(tt.args.resp, "Success!") + } + + }) + } +} + +func TestPut(t *testing.T) { + assertions := require.New(t) + + type args struct { + header string + payload interface{} + respCode int + resp interface{} + userInfo [2]string + } + tests := []struct { + name string + args args + wantErr string + }{ + { + name: "successful PUT request", + args: args{ + header: "application/json", + payload: `json:"example"`, + respCode: http.StatusOK, + resp: "Success!", + userInfo: [2]string{"user", "pass"}, + }, + wantErr: "", + }, + { + name: "error PUT request", + args: args{ + header: "application/json", + payload: `json:"example"`, + respCode: http.StatusBadRequest, + resp: nil, + }, + wantErr: "error response with status: 400 and body:", + }, + { + name: "error to create PUT request", + args: args{ + header: "application/json", + payload: map[string]interface{}{ + "foo": make(chan int), + }, + respCode: http.StatusBadRequest, + resp: nil, + }, + wantErr: "failed to marshal request body: json: unsupported type: chan int", + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + assert.Equal(t, http.MethodPut, r.Method) + assert.Contains(t, r.Header.Get("Content-Type"), "application/json") + + var reqBody string + decoder := json.NewDecoder(r.Body) + decoder.Decode(&reqBody) + assert.Equal(t, reqBody, `json:"example"`) + + response, _ := json.Marshal(tt.args.resp) + w.Header().Set("Content-Type", tt.args.header) + w.WriteHeader(tt.args.respCode) + w.Write(response) + })) + defer srv.Close() + + client := New(&http.Client{}, false) + var err error + if tt.args.userInfo[0] != "" { + err = client.Put(srv.URL, tt.args.payload, nil, tt.args.userInfo[0], tt.args.userInfo[1]) + } else { + err = client.Put(srv.URL, tt.args.payload, nil) + } + + if err != nil { + assertions.Contains(err.Error(), tt.wantErr) + } else { + assertions.Equal(tt.args.resp, "Success!") + } + + }) + } +} + +func TestDelete(t *testing.T) { + header := "application/json" + respCode := http.StatusOK + resp := "Success!" + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + assert.Equal(t, http.MethodDelete, r.Method) + assert.Contains(t, r.Header.Get("Content-Type"), "application/json") + + var reqBody string + decoder := json.NewDecoder(r.Body) + decoder.Decode(&reqBody) + assert.Equal(t, reqBody, `json:"example"`) + + response, _ := json.Marshal(resp) + w.Header().Set("Content-Type", header) + w.WriteHeader(respCode) + w.Write(response) + })) + defer srv.Close() + + client := New(&http.Client{}, false) + payload := `json:"example"` + err := client.Delete(srv.URL, payload, nil, "admin", "pass") + + if err != nil { + assert.Equal(t, "", err.Error()) + } +} diff --git a/icsversion/internal/structures/measurements.go b/icsversion/internal/structures/measurements.go new file mode 100644 index 0000000..2fa5846 --- /dev/null +++ b/icsversion/internal/structures/measurements.go @@ -0,0 +1,88 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 structures + +import "oransc.org/usecase/oduclosedloop/icsversion/messages" + +type SliceMetric struct { + DUId string + CellId string + SliceDiff int + SliceServiceType int + RRMPolicyRatioId string + PM map[string]int +} + +func NewSliceMetric(duid string, cellid string, sd int, sst int) *SliceMetric { + sm := SliceMetric{ + DUId: duid, + CellId: cellid, + SliceDiff: sd, + SliceServiceType: sst, + } + sm.PM = make(map[string]int) + return &sm +} + +type PolicyRatio struct { + PolicyRatioId string + PolicyMaxRatio int + PolicyMinRatio int + PolicyDedicatedRatio int +} + +func NewPolicyRatio(id string, max_ratio int, min_ratio int, ded_ratio int) *PolicyRatio { + pr := PolicyRatio{ + PolicyRatioId: id, + PolicyMaxRatio: max_ratio, + PolicyMinRatio: min_ratio, + PolicyDedicatedRatio: ded_ratio, + } + return &pr +} + +func (pr *PolicyRatio) GetUpdateDedicatedRatioMessage(sd int, sst int, dedicatedRatio int) interface{} { + message := messages.RRMPolicyRatio{ + Id: pr.PolicyRatioId, + AdmState: "unlocked", + UserLabel: pr.PolicyRatioId, + RRMPolicyMaxRatio: pr.PolicyMaxRatio, + RRMPolicyMinRatio: pr.PolicyMinRatio, + RRMPolicyDedicatedRatio: dedicatedRatio, + ResourceType: "prb", + RRMPolicyMembers: []messages.RRMPolicyMember{ + { + MobileCountryCode: "046", + MobileNetworkCode: "651", + SliceDifferentiator: sd, + SliceServiceType: sst, + }, + }, + } + rrmPolicies := []messages.RRMPolicyRatio{message} + + return struct { + RRMPolicies []messages.RRMPolicyRatio `json:"radio-resource-management-policy-ratio"` + }{ + RRMPolicies: rrmPolicies, + } + +} diff --git a/icsversion/internal/structures/sliceassurance.go b/icsversion/internal/structures/sliceassurance.go new file mode 100644 index 0000000..f4c5f5f --- /dev/null +++ b/icsversion/internal/structures/sliceassurance.go @@ -0,0 +1,131 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 structures + +import ( + "fmt" + "regexp" + "strconv" + + log "github.com/sirupsen/logrus" + "oransc.org/usecase/oduclosedloop/icsversion/messages" +) + +type MapKey struct { + Duid string + sd int + sst int +} + +type SliceAssuranceMeas struct { + Metrics map[MapKey]*SliceMetric + Policies map[string]*PolicyRatio +} + +func NewSliceAssuranceMeas() *SliceAssuranceMeas { + s := SliceAssuranceMeas{} + s.Metrics = make(map[MapKey]*SliceMetric) + s.Policies = make(map[string]*PolicyRatio) + return &s +} + +func (sa *SliceAssuranceMeas) AddNewPolicy(duid string, rrmPolicyRatio messages.RRMPolicyRatio) { + for _, policyMember := range rrmPolicyRatio.RRMPolicyMembers { + metric := sa.GetSliceMetric(duid, policyMember.SliceDifferentiator, policyMember.SliceServiceType) + if metric != nil { + pr := NewPolicyRatio(rrmPolicyRatio.Id, rrmPolicyRatio.RRMPolicyMaxRatio, rrmPolicyRatio.RRMPolicyMinRatio, rrmPolicyRatio.RRMPolicyDedicatedRatio) + _, check := sa.Policies[pr.PolicyRatioId] + if !check { + log.Infof(" new policy has been added %+v", *pr) + } + sa.Policies[pr.PolicyRatioId] = pr + metric.RRMPolicyRatioId = rrmPolicyRatio.Id + + } + } +} + +func (sa *SliceAssuranceMeas) GetSliceMetric(duid string, sd int, sst int) *SliceMetric { + key := MapKey{duid, sd, sst} + value, check := sa.Metrics[key] + + if check { + return value + } + + return nil +} + +func (sa *SliceAssuranceMeas) AddOrUpdateMetric(meas messages.Measurement) (string, error) { + + var duid string + var sd, sst int + + regex := *regexp.MustCompile(`\/(.*)network-function\/distributed-unit-functions\[id=\'(.*)\'\]\/cell\[id=\'(.*)\'\]\/supported-measurements\[performance-measurement-type=\'(.*)\'\]\/supported-snssai-subcounter-instances\[slice-differentiator=\'(\d+)\'\]\[slice-service-type=\'(\d+)\'\]`) + res := regex.FindAllStringSubmatch(meas.MeasurementTypeInstanceReference, -1) + + if res != nil && len(res[0]) == 7 { + duid = res[0][2] + sd = toInt(res[0][5]) + sst = toInt(res[0][6]) + + key := MapKey{duid, sd, sst} + value, check := sa.Metrics[key] + + if check { + sa.updateMetric(key, value, res[0][4], meas.Value) + } else { + // Only add new one if value exceeds threshold + sa.addMetric(res, meas.Value) + } + } else { + return duid, fmt.Errorf(" wrong format for MeasurementTypeInstanceReference") + } + return duid, nil +} + +func (sa *SliceAssuranceMeas) addMetric(res [][]string, metricValue int) { + if metricValue > 700 { + metric := NewSliceMetric(res[0][2], res[0][3], toInt(res[0][5]), toInt(res[0][6])) + metric.PM[res[0][3]] = metricValue + key := MapKey{res[0][2], toInt(res[0][5]), toInt(res[0][6])} + sa.Metrics[key] = metric + log.Infof(" new metric has been added %+v", *metric) + } +} + +func (sa *SliceAssuranceMeas) updateMetric(key MapKey, value *SliceMetric, metricName string, metricValue int) { + if metricValue < 700 { + delete(sa.Metrics, key) + log.Infof(" metric with key %+v has been deleted", key) + } else { + value.PM[metricName] = metricValue + log.Infof(" metric value has been updated, new value: %v", metricValue) + } +} + +func toInt(num string) int { + res, err := strconv.Atoi(num) + if err != nil { + return -1 + } + return res +} diff --git a/icsversion/internal/structures/sliceassurance_test.go b/icsversion/internal/structures/sliceassurance_test.go new file mode 100644 index 0000000..97d3e32 --- /dev/null +++ b/icsversion/internal/structures/sliceassurance_test.go @@ -0,0 +1,171 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 structures + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "oransc.org/usecase/oduclosedloop/icsversion/messages" +) + +func TestAddMetric(t *testing.T) { + assertions := require.New(t) + type args struct { + meas messages.Measurement + } + tests := []struct { + name string + args args + }{ + { + name: "Test adding new metric", + args: args{ + meas: messages.Measurement{ + MeasurementTypeInstanceReference: "/o-ran-sc-du-hello-world:network-function/distributed-unit-functions[id='O-DU-1211']/cell[id='cell-1']/supported-measurements[performance-measurement-type='user-equipment-average-throughput-uplink']/supported-snssai-subcounter-instances[slice-differentiator='1'][slice-service-type='1']", + Value: 51232, + Unit: "kbit/s", + }, + }, + }, + { + name: "Test with invalid input", + args: args{ + meas: messages.Measurement{ + MeasurementTypeInstanceReference: "/distributed-unit-functions[id='O-DU-1211']/cell[id='cell-1']/supported-measurements[performance-measurement-type='user-equipment-average-throughput-uplink']/supported-snssai-subcounter-instances[slice-differentiator='1'][slice-service-type='1']", + Value: 51232, + Unit: "kbit/s", + }, + }, + }, + } + + sliceAssuranceMeas := NewSliceAssuranceMeas() + assertions.Equal(0, len(sliceAssuranceMeas.Metrics), "Metrics is not empty, got: %d, want: %d.", len(sliceAssuranceMeas.Metrics), 0) + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + if i == 0 { + sliceAssuranceMeas.AddOrUpdateMetric(tt.args.meas) + assertions.Equal(1, len(sliceAssuranceMeas.Metrics), "Metrics must have one new metric, got: %d, want: %d.", len(sliceAssuranceMeas.Metrics), 1) + + testMapKey := MapKey{"O-DU-1211", 1, 1} + assertions.Contains(sliceAssuranceMeas.Metrics, testMapKey, "Metric added with wrong values , got: %v.", sliceAssuranceMeas.Metrics[testMapKey]) + } + if i == 1 { + _, got := sliceAssuranceMeas.AddOrUpdateMetric(tt.args.meas) + assertions.EqualError(got, " wrong format for MeasurementTypeInstanceReference") + } + }) + } +} + +func TestUpdateExistingMetric(t *testing.T) { + assertions := require.New(t) + meas := messages.Measurement{ + MeasurementTypeInstanceReference: "/o-ran-sc-du-hello-world:network-function/distributed-unit-functions[id='O-DU-1211']/cell[id='cell-1']/supported-measurements[performance-measurement-type='user-equipment-average-throughput-uplink']/supported-snssai-subcounter-instances[slice-differentiator='1'][slice-service-type='1']", + Value: 51232, + Unit: "kbit/s", + } + + updateMeas := messages.Measurement{ + MeasurementTypeInstanceReference: "/o-ran-sc-du-hello-world:network-function/distributed-unit-functions[id='O-DU-1211']/cell[id='cell-1']/supported-measurements[performance-measurement-type='user-equipment-average-throughput-uplink']/supported-snssai-subcounter-instances[slice-differentiator='1'][slice-service-type='1']", + Value: 897, + Unit: "kbit/s", + } + + sliceAssuranceMeas := NewSliceAssuranceMeas() + assertions.Equal(0, len(sliceAssuranceMeas.Metrics), "Metrics is not empty, got: %d, want: %d.", len(sliceAssuranceMeas.Metrics), 0) + + sliceAssuranceMeas.AddOrUpdateMetric(meas) + assertions.Equal(1, len(sliceAssuranceMeas.Metrics), "Metrics must have one new metric, got: %d, want: %d.", len(sliceAssuranceMeas.Metrics), 1) + + sliceAssuranceMeas.AddOrUpdateMetric(updateMeas) + assertions.Equal(1, len(sliceAssuranceMeas.Metrics), "Metrics must have one updated metric, got: %d, want: %d.", len(sliceAssuranceMeas.Metrics), 1) + + testMapKey := MapKey{"O-DU-1211", 1, 1} + metricName := "user-equipment-average-throughput-uplink" + newMetricValue := 897 + if sliceAssuranceMeas.Metrics[testMapKey].PM[metricName] != newMetricValue { + t.Errorf("Metric value was not update properly, got: %d, want: %d.", sliceAssuranceMeas.Metrics[testMapKey].PM[metricName], newMetricValue) + } + +} + +func TestDeleteMetricWhenValueLessThanThreshold(t *testing.T) { + + meas := messages.Measurement{ + MeasurementTypeInstanceReference: "/o-ran-sc-du-hello-world:network-function/distributed-unit-functions[id='O-DU-1211']/cell[id='cell-1']/supported-measurements[performance-measurement-type='user-equipment-average-throughput-uplink']/supported-snssai-subcounter-instances[slice-differentiator='1'][slice-service-type='1']", + Value: 51232, + Unit: "kbit/s", + } + + newMeas := messages.Measurement{ + MeasurementTypeInstanceReference: "/o-ran-sc-du-hello-world:network-function/distributed-unit-functions[id='O-DU-1211']/cell[id='cell-1']/supported-measurements[performance-measurement-type='user-equipment-average-throughput-uplink']/supported-snssai-subcounter-instances[slice-differentiator='1'][slice-service-type='1']", + Value: 50, + Unit: "kbit/s", + } + + sliceAssuranceMeas := NewSliceAssuranceMeas() + assert.Equal(t, 0, len(sliceAssuranceMeas.Metrics), "Metrics is not empty, got: %d, want: %d.", len(sliceAssuranceMeas.Metrics), 0) + + sliceAssuranceMeas.AddOrUpdateMetric(meas) + assert.Equal(t, 1, len(sliceAssuranceMeas.Metrics), "Metrics must have one new metric, got: %d, want: %d.", len(sliceAssuranceMeas.Metrics), 1) + + sliceAssuranceMeas.AddOrUpdateMetric(newMeas) + assert.Equal(t, 0, len(sliceAssuranceMeas.Metrics), "Metrics must have been deleted, got: %d, want: %d.", len(sliceAssuranceMeas.Metrics), 0) + +} + +func TestAddPolicy(t *testing.T) { + + meas := messages.Measurement{ + MeasurementTypeInstanceReference: "/o-ran-sc-du-hello-world:network-function/distributed-unit-functions[id='O-DU-1211']/cell[id='cell-1']/supported-measurements[performance-measurement-type='user-equipment-average-throughput-uplink']/supported-snssai-subcounter-instances[slice-differentiator='1'][slice-service-type='1']", + Value: 51232, + Unit: "kbit/s", + } + sliceAssuranceMeas := NewSliceAssuranceMeas() + sliceAssuranceMeas.AddOrUpdateMetric(meas) + + duid := "O-DU-1211" + rrmPolicyRatio := messages.RRMPolicyRatio{ + Id: "id", + AdmState: "locked", + UserLabel: "user_label", + RRMPolicyMaxRatio: 0, + RRMPolicyMinRatio: 0, + RRMPolicyDedicatedRatio: 0, + ResourceType: "prb", + RRMPolicyMembers: []messages.RRMPolicyMember{{ + MobileCountryCode: "046", + MobileNetworkCode: "651", + SliceDifferentiator: 1, + SliceServiceType: 1, + }}, + } + assert.Equal(t, 0, len(sliceAssuranceMeas.Policies), "Policies is not empty, got: %d, want: %d.", len(sliceAssuranceMeas.Policies), 0) + + sliceAssuranceMeas.AddNewPolicy(duid, rrmPolicyRatio) + assert.Equal(t, 1, len(sliceAssuranceMeas.Policies), "Policies must have one new policy, got: %d, want: %d.", len(sliceAssuranceMeas.Policies), 1) + +} diff --git a/icsversion/main.go b/icsversion/main.go new file mode 100644 index 0000000..b9e9afc --- /dev/null +++ b/icsversion/main.go @@ -0,0 +1,71 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 ( + "fmt" + "os" + "os/signal" + "syscall" + + log "github.com/sirupsen/logrus" + "oransc.org/usecase/oduclosedloop/icsversion/internal/config" + sliceassurance "oransc.org/usecase/oduclosedloop/icsversion/internal/odusliceassurance" +) + +var configuration *config.Configuration +var a sliceassurance.App + +const TOPIC string = "/events/unauthenticated.VES_O_RAN_SC_HELLO_WORLD_PM_STREAMING_OUTPUT/myG/C1" + +func main() { + configuration = config.New() + + log.SetLevel(configuration.LogLevel) + log.SetFormatter(&log.JSONFormatter{}) + + log.Debug("Using configuration: ", configuration) + + if err := validateConfiguration(configuration); err != nil { + log.Fatalf("Unable to start consumer due to configuration error: %v", err) + } + + a = sliceassurance.App{} + a.Initialize(configuration) + + go a.StartServer() + keepConsumerAlive() +} + +func validateConfiguration(configuration *config.Configuration) error { + if configuration.ConsumerHost == "" || configuration.ConsumerPort == 0 { + return fmt.Errorf("consumer host and port must be provided") + } + return nil +} + +func keepConsumerAlive() { + exitSignal := make(chan os.Signal, 1) + signal.Notify(exitSignal, syscall.SIGINT, syscall.SIGTERM) + <-exitSignal + + a.Teardown() +} diff --git a/icsversion/messages/policyRatio.go b/icsversion/messages/policyRatio.go new file mode 100644 index 0000000..bbfa1a0 --- /dev/null +++ b/icsversion/messages/policyRatio.go @@ -0,0 +1,105 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 messages + +type ORanDuRestConf struct { + DistributedUnitFunction []DistributedUnitFunction `json:"o-ran-sc-du-hello-world:distributed-unit-functions"` +} + +type DistributedUnitFunction struct { + Id string `json:"id"` + OperationalState string `json:"operational-state"` + AdmState string `json:"administrative-state"` + UserLabel string `json:"user-label"` + RRMPolicyRatio []RRMPolicyRatio `json:"radio-resource-management-policy-ratio"` + Cell []Cell `json:"cell"` +} + +type RRMPolicyRatio struct { + Id string `json:"id"` + AdmState string `json:"administrative-state"` + UserLabel string `json:"user-label"` + RRMPolicyMaxRatio int `json:"radio-resource-management-policy-max-ratio"` + RRMPolicyMinRatio int `json:"radio-resource-management-policy-min-ratio"` + RRMPolicyDedicatedRatio int `json:"radio-resource-management-policy-dedicated-ratio"` + ResourceType string `json:"resource-type"` + RRMPolicyMembers []RRMPolicyMember `json:"radio-resource-management-policy-members"` +} + +type RRMPolicyMember struct { + MobileCountryCode string `json:"mobile-country-code"` + MobileNetworkCode string `json:"mobile-network-code"` + SliceDifferentiator int `json:"slice-differentiator"` + SliceServiceType int `json:"slice-service-type"` +} + +type Cell struct { + Id string `json:"id"` + LocalId int `json:"local-id"` + PhysicalCellId int `json:"physical-cell-id"` + BaseStationChannelBandwidth BaseStationChannelBandwidth `json:"base-station-channel-bandwidth"` + OperationalState string `json:"operational-state"` + TrackingAreaCode int `json:"tracking-area-code"` + AdmState string `json:"administrative-state"` + PublicLandMobileNetworks []PublicLandMobileNetworks `json:"public-land-mobile-networks"` + SupportedMeasurements []SupportedMeasurements `json:"supported-measurements"` + TrafficState string `json:"traffic-state"` + AbsoluteRadioFrequencyChannelNumber AbsoluteRadioFrequencyChannelNumber `json:"absolute-radio-frequency-channel-number"` + UserLabel string `json:"user-label"` + SynchronizationSignalBlock SynchronizationSignalBlock `json:"synchronization-signal-block"` +} + +type BaseStationChannelBandwidth struct { + Uplink int `json:"uplink"` + Downlink int `json:"downlink"` + SupplementaryUplink int `json:"supplementary-uplink"` +} + +type PublicLandMobileNetworks struct { + SliceDifferentiator int `json:"slice-differentiator"` + SliceServiceType int `json:"slice-service-type"` + MobileCountryCode string `json:"mobile-country-code"` + MobileNetworkCode string `json:"mobile-network-code"` +} + +type SupportedMeasurements struct { + PerformanceMeasurementType string `json:"performance-measurement-type"` + SupportedSnssaiSubcounterInstances []SupportedSnssaiSubcounterInstances `json:"supported-snssai-subcounter-instances"` +} + +type SupportedSnssaiSubcounterInstances struct { + SliceDifferentiator int `json:"slice-differentiator"` + SliceServiceType int `json:"slice-service-type"` +} + +type AbsoluteRadioFrequencyChannelNumber struct { + Uplink int `json:"uplink"` + Downlink int `json:"downlink"` + SupplementaryUplink int `json:"supplementary-uplink"` +} + +type SynchronizationSignalBlock struct { + Duration int `json:"duration"` + FrequencyChannelNumber int `json:"frequency-channel-number"` + Periodicity int `json:"periodicity"` + SubcarrierSpacing int `json:"subcarrier-spacing"` + Offset int `json:"offset"` +} diff --git a/icsversion/messages/stdVesMessage.go b/icsversion/messages/stdVesMessage.go new file mode 100644 index 0000000..afb786f --- /dev/null +++ b/icsversion/messages/stdVesMessage.go @@ -0,0 +1,78 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 messages + +type StdDefinedMessage struct { + Event Event `json:"event"` +} + +type Event struct { + CommonEventHeader CommonEventHeader `json:"commonEventHeader"` + StndDefinedFields StndDefinedFields `json:"stndDefinedFields"` +} + +type CommonEventHeader struct { + Domain string `json:"domain"` + EventId string `json:"eventId"` + EventName string `json:"eventName"` + EventType string `json:"eventType"` + Sequence int `json:"sequence"` + Priority string `json:"priority"` + ReportingEntityId string `json:"reportingEntityId"` + ReportingEntityName string `json:"reportingEntityName"` + SourceId string `json:"sourceId"` + SourceName string `json:"sourceName"` + StartEpochMicrosec int64 `json:"startEpochMicrosec"` + LastEpochMicrosec int64 `json:"lastEpochMicrosec"` + NfNamingCode string `json:"nfNamingCode"` + NfVendorName string `json:"nfVendorName"` + StndDefinedNamespace string `json:"stndDefinedNamespace"` + TimeZoneOffset string `json:"timeZoneOffset"` + Version string `json:"version"` + VesEventListenerVersion string `json:"vesEventListenerVersion"` +} + +type StndDefinedFields struct { + StndDefinedFieldsVersion string `json:"stndDefinedFieldsVersion"` + SchemaReference string `json:"schemaReference"` + Data Data `json:"data"` +} + +type Data struct { + DataId string `json:"id"` + StartTime string `json:"start-time"` + AdministrativeState string `json:"administrative-state"` + OperationalState string `json:"operational-state"` + UserLabel string `json:"user-label"` + JobTag string `json:"job-tag"` + GranularityPeriod int `json:"granularity-period"` + Measurements []Measurement `json:"measurements"` +} + +type Measurement struct { + MeasurementTypeInstanceReference string `json:"measurement-type-instance-reference"` + Value int `json:"value"` + Unit string `json:"unit"` +} + +func (message StdDefinedMessage) GetMeasurements() []Measurement { + return message.Event.StndDefinedFields.Data.Measurements +} diff --git a/icsversion/messages/stdVesMessage_test.go b/icsversion/messages/stdVesMessage_test.go new file mode 100644 index 0000000..ada6d6e --- /dev/null +++ b/icsversion/messages/stdVesMessage_test.go @@ -0,0 +1,86 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 messages + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetMeasurements(t *testing.T) { + assertions := require.New(t) + type fields struct { + Event Event + } + tests := []struct { + name string + fields fields + want []Measurement + }{ + { + name: "get measurements message", + fields: fields{ + Event: Event{ + CommonEventHeader: CommonEventHeader{ + Domain: "stndDefined", + StndDefinedNamespace: "o-ran-sc-du-hello-world-pm-streaming-oas3", + }, + StndDefinedFields: StndDefinedFields{ + StndDefinedFieldsVersion: "1.0", + SchemaReference: "https://gerrit.o-ran-sc.org/r/gitweb?p=scp/oam/modeling.git;a=blob_plain;f=data-model/oas3/experimental/o-ran-sc-du-hello-world-oas3.json;hb=refs/heads/master", + Data: Data{ + DataId: "id", + Measurements: []Measurement{{ + MeasurementTypeInstanceReference: "/o-ran-sc-du-hello-world:network-function/distributed-unit-functions[id='O-DU-1211']/cell[id='cell-1']/supported-measurements[performance-measurement-type='user-equipment-average-throughput-uplink']/supported-snssai-subcounter-instances[slice-differentiator='1'][slice-service-type='1']", + Value: 51232, + Unit: "kbit/s", + }}, + }, + }, + }, + }, + want: []Measurement{{ + MeasurementTypeInstanceReference: "/o-ran-sc-du-hello-world:network-function/distributed-unit-functions[id='O-DU-1211']/cell[id='cell-1']/supported-measurements[performance-measurement-type='user-equipment-average-throughput-uplink']/supported-snssai-subcounter-instances[slice-differentiator='1'][slice-service-type='1']", + Value: 51232, + Unit: "kbit/s", + }}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := StdDefinedMessage{ + Event: tt.fields.Event, + } + var got []Measurement + if got = message.GetMeasurements(); len(got) != len(tt.want) { + t.Errorf("Message.GetMeasurements() = %v, want %v", got, tt.want) + } + + for _, meas := range got { + assertions.Equal(51232, meas.Value) + assertions.Contains(meas.MeasurementTypeInstanceReference, "user-equipment-average-throughput-uplink") + } + + }) + } +} diff --git a/icsversion/stub/DmaapSdnc/dmaapSdncStub.go b/icsversion/stub/DmaapSdnc/dmaapSdncStub.go new file mode 100644 index 0000000..3f7ea64 --- /dev/null +++ b/icsversion/stub/DmaapSdnc/dmaapSdncStub.go @@ -0,0 +1,429 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 ( + "bytes" + "encoding/csv" + "encoding/json" + "flag" + "fmt" + "math/rand" + "net/http" + "os" + "strconv" + "sync" + "time" + + "github.com/gorilla/mux" + "oransc.org/usecase/oduclosedloop/icsversion/messages" + + log "github.com/sirupsen/logrus" +) + +const THRESHOLD_TPUT int = 7000 + +type SliceAssuranceInformation struct { + duId string + cellId string + sd int + sst int + metricName string + metricValue int + policyRatioId string + policyMaxRatio int + policyMinRatio int + policyDedicatedRatio int +} + +var data []*SliceAssuranceInformation +var messagesToSend []messages.Measurement +var started bool + +func loadData() { + lines, err := GetCsvFromFile("test-data.csv") + if err != nil { + panic(err) + } + for _, line := range lines { + sai := SliceAssuranceInformation{ + duId: line[0], + cellId: line[1], + sd: toInt(line[2]), + sst: toInt(line[3]), + metricName: line[4], + metricValue: toInt(line[5]), + policyRatioId: line[6], + policyMaxRatio: toInt(line[7]), + policyMinRatio: toInt(line[8]), + policyDedicatedRatio: toInt(line[9]), + } + data = append(data, &sai) + } +} + +func 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 + } +} + +func toInt(num string) int { + res, err := strconv.Atoi(num) + if err != nil { + return -1 + } + return res +} + +func main() { + rand.Seed(time.Now().UnixNano()) + + portSdnr := flag.Int("sdnr-port", 3904, "The port this SDNR stub will listen on") + dmaapProducerPort := flag.Int("dmaap-port", 3905, "The port this Dmaap mediator will listen on") + flag.Parse() + + loadData() + + wg := new(sync.WaitGroup) + wg.Add(2) + + go func() { + + r := mux.NewRouter() + r.HandleFunc("/rests/data/network-topology:network-topology/topology=topology-netconf/node={NODE-ID}/yang-ext:mount/o-ran-sc-du-hello-world:network-function/distributed-unit-functions={O-DU-ID}", getSdnrResponseMessage).Methods(http.MethodGet) + r.HandleFunc("/rests/data/network-topology:network-topology/topology=topology-netconf/node={NODE-ID}/yang-ext:mount/o-ran-sc-du-hello-world:network-function/distributed-unit-functions={O-DU-ID}/radio-resource-management-policy-ratio={POLICY-ID}", updateRRMPolicyDedicatedRatio).Methods(http.MethodPut) + + fmt.Println("Starting SDNR stub on port: ", *portSdnr) + + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", *portSdnr), r)) + wg.Done() + }() + + go func() { + + r := mux.NewRouter() + r.HandleFunc("/create/{jobId}", createJobHandler).Methods(http.MethodPut) + r.HandleFunc("/delete/{jobId}", deleteJobHandler).Methods(http.MethodDelete) + + fmt.Println("Producer listening on port: ", *dmaapProducerPort) + + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", *dmaapProducerPort), r)) + wg.Done() + }() + + wg.Wait() +} + +func createJobHandler(w http.ResponseWriter, r *http.Request) { + fmt.Println("createJobHandler:: ", r) + vars := mux.Vars(r) + id, ok := vars["jobId"] + if !ok { + http.Error(w, "No job ID provided", http.StatusBadRequest) + return + } + + started = true + fmt.Println("Start pushing messages for job: ", id) + go sendDmaapMessages() +} + +func deleteJobHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, ok := vars["jobId"] + if !ok { + http.Error(w, "No job ID provided", http.StatusBadRequest) + return + } + + fmt.Println("Stop pushing messages for job: ", id) + started = false +} + +func getSdnrResponseMessage(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + log.Info("Get messages for RRM Policy Ratio information for O-Du ID ", vars["O-DU-ID"]) + + distUnitFunctions := getDistributedUnitFunctionMessage(vars["O-DU-ID"]) + + respondWithJSON(w, http.StatusOK, distUnitFunctions) +} + +func getDistributedUnitFunctionMessage(oduId string) messages.ORanDuRestConf { + + var policies []messages.RRMPolicyRatio + for _, entry := range data { + message := messages.RRMPolicyRatio{ + Id: entry.policyRatioId, + AdmState: "locked", + UserLabel: entry.policyRatioId, + RRMPolicyMaxRatio: entry.policyMaxRatio, + RRMPolicyMinRatio: entry.policyMinRatio, + RRMPolicyDedicatedRatio: entry.policyDedicatedRatio, + ResourceType: "prb", + RRMPolicyMembers: []messages.RRMPolicyMember{ + { + MobileCountryCode: "310", + MobileNetworkCode: "150", + SliceDifferentiator: entry.sd, + SliceServiceType: entry.sst, + }, + }, + } + policies = append(policies, message) + } + + var publicLandMobileNetworks []messages.PublicLandMobileNetworks + for _, entry := range data { + publicLandMobileNetwork := messages.PublicLandMobileNetworks{ + MobileCountryCode: "310", + MobileNetworkCode: "150", + SliceDifferentiator: entry.sd, + SliceServiceType: entry.sst, + } + publicLandMobileNetworks = append(publicLandMobileNetworks, publicLandMobileNetwork) + } + + var supportedSnssaiSubcounterInstances []messages.SupportedSnssaiSubcounterInstances + for _, entry := range data { + supportedSnssaiSubcounterInstance := messages.SupportedSnssaiSubcounterInstances{ + SliceDifferentiator: entry.sd, + SliceServiceType: entry.sst, + } + supportedSnssaiSubcounterInstances = append(supportedSnssaiSubcounterInstances, supportedSnssaiSubcounterInstance) + } + + cell := messages.Cell{ + Id: "cell-1", + LocalId: 1, + PhysicalCellId: 1, + BaseStationChannelBandwidth: messages.BaseStationChannelBandwidth{ + Uplink: 83000, + Downlink: 80000, + SupplementaryUplink: 84000, + }, + OperationalState: "enabled", + TrackingAreaCode: 10, + AdmState: "unlocked", + PublicLandMobileNetworks: publicLandMobileNetworks, + SupportedMeasurements: []messages.SupportedMeasurements{ + { + PerformanceMeasurementType: "o-ran-sc-du-hello-world:user-equipment-average-throughput-uplink", + SupportedSnssaiSubcounterInstances: supportedSnssaiSubcounterInstances, + }, + { + PerformanceMeasurementType: "o-ran-sc-du-hello-world:user-equipment-average-throughput-downlink", + SupportedSnssaiSubcounterInstances: supportedSnssaiSubcounterInstances, + }, + }, + TrafficState: "active", + AbsoluteRadioFrequencyChannelNumber: messages.AbsoluteRadioFrequencyChannelNumber{ + Uplink: 14000, + Downlink: 15000, + SupplementaryUplink: 14500, + }, + UserLabel: "cell-1", + SynchronizationSignalBlock: messages.SynchronizationSignalBlock{ + Duration: 2, + FrequencyChannelNumber: 12, + Periodicity: 10, + SubcarrierSpacing: 30, + Offset: 3, + }, + } + + distUnitFunction := messages.DistributedUnitFunction{ + Id: oduId, + OperationalState: "enabled", + AdmState: "unlocked", + UserLabel: oduId, + Cell: []messages.Cell{ + cell, + }, + RRMPolicyRatio: policies, + } + + duRRMPolicyRatio := messages.ORanDuRestConf{ + DistributedUnitFunction: []messages.DistributedUnitFunction{ + distUnitFunction, + }, + } + + return duRRMPolicyRatio +} + +func updateRRMPolicyDedicatedRatio(w http.ResponseWriter, r *http.Request) { + var policies struct { + RRMPolicies []messages.RRMPolicyRatio `json:"radio-resource-management-policy-ratio"` + } + decoder := json.NewDecoder(r.Body) + + if err := decoder.Decode(&policies); err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid request payload") + return + } + defer r.Body.Close() + + prMessages := policies.RRMPolicies + log.Infof("Post request to update RRMPolicyDedicatedRatio %+v", prMessages) + findAndUpdatePolicy(prMessages) + respondWithJSON(w, http.StatusOK, map[string]string{"status": "200"}) +} + +func findAndUpdatePolicy(rRMPolicyRatio []messages.RRMPolicyRatio) { + for _, policy := range rRMPolicyRatio { + for _, entry := range data { + if entry.policyRatioId == policy.Id { + log.Infof("update Policy Dedicated Ratio: value for policy %+v\n Old value: %v New value: %v ", policy, entry.policyDedicatedRatio, policy.RRMPolicyDedicatedRatio) + entry.policyDedicatedRatio = policy.RRMPolicyDedicatedRatio + if entry.metricValue > THRESHOLD_TPUT { + entry.metricValue = rand.Intn(THRESHOLD_TPUT) + } + messagesToSend = append(messagesToSend, generateMeasurementEntry(entry)) + } + } + } +} + +func respondWithError(w http.ResponseWriter, code int, message string) { + respondWithJSON(w, code, map[string]string{"error": message}) +} + +func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { + response, _ := json.Marshal(payload) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + w.Write(response) +} + +func sendDmaapMessages() { + + client := &http.Client{ + Timeout: 10 * time.Second, + } + + log.Info("Send Dmaap messages") + for range time.Tick(10 * time.Second) { + if !started { + break + } + m, _ := json.Marshal(generateStdMessage()) + msgToSend, _ := json.Marshal([]string{string(m)}) + + time.Sleep(time.Duration(rand.Intn(3)) * time.Second) + + oru_addr := getEnv("ORU_ADDR", "http://localhost:8095") + req, _ := http.NewRequest(http.MethodPost, oru_addr, bytes.NewBuffer(msgToSend)) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + _, err := client.Do(req) + if err != nil { + fmt.Println("Error sending to consumer: ", err) + } + fmt.Println("Sent message to consumer!") + } +} + +func getEnv(key string, defaultVal string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + + return defaultVal +} + +func generateStdMessage() messages.StdDefinedMessage { + entry := data[rand.Intn(5)] + + maxTput := THRESHOLD_TPUT + 100 + randomTput := rand.Intn(maxTput-THRESHOLD_TPUT+1) + THRESHOLD_TPUT + if randomTput%3 == 0 { + log.Info("Using tput value higher than THRESHOLD_TPUT ", randomTput) + entry.metricValue = randomTput + } + + messagesToSend = append(messagesToSend, generateMeasurementEntry(entry)) + + message := messages.StdDefinedMessage{ + Event: messages.Event{ + CommonEventHeader: messages.CommonEventHeader{ + Domain: "stndDefined", + EventId: "pm-1_1644252450", + EventName: "stndDefined_performanceMeasurementStreaming", + EventType: "performanceMeasurementStreaming", + Sequence: 825, + Priority: "Low", + ReportingEntityId: "", + ReportingEntityName: "O-DU-1122", + SourceId: "", + SourceName: "O-DU-1122", + StartEpochMicrosec: 1644252450000000, + LastEpochMicrosec: 1644252480000000, + NfNamingCode: "SIM-O-DU", + NfVendorName: "O-RAN-SC SIM Project", + StndDefinedNamespace: "o-ran-sc-du-hello-world-pm-streaming-oas3", + TimeZoneOffset: "+00:00", + Version: "4.1", + VesEventListenerVersion: "7.2.1", + }, + StndDefinedFields: messages.StndDefinedFields{ + StndDefinedFieldsVersion: "1.0", + SchemaReference: "https://gerrit.o-ran-sc.org/r/gitweb?p=scp/oam/modeling.git;a=blob_plain;f=data-model/oas3/experimental/o-ran-sc-du-hello-world-oas3.json;hb=refs/heads/master", + Data: messages.Data{ + DataId: "pm-1_1644252450", + StartTime: "2022-02-07T16:47:30.0Z", + AdministrativeState: "unlocked", + OperationalState: "enabled", + UserLabel: "pm", + JobTag: "my-job-tag", + GranularityPeriod: 30, + Measurements: messagesToSend, + }, + }, + }, + } + messagesToSend = nil + fmt.Printf("Sending Dmaap message:\n %+v\n", message) + return message +} + +func generateMeasurementEntry(entry *SliceAssuranceInformation) messages.Measurement { + + measurementTypeInstanceReference := "/o-ran-sc-du-hello-world:network-function/distributed-unit-functions[id='" + entry.duId + "']/cell[id='" + entry.cellId + "']/supported-measurements[performance-measurement-type='(urn:o-ran-sc:yang:o-ran-sc-du-hello-world?revision=2021-11-23)" + entry.metricName + "']/supported-snssai-subcounter-instances[slice-differentiator='" + strconv.Itoa(entry.sd) + "'][slice-service-type='" + strconv.Itoa(entry.sst) + "']" + meas := messages.Measurement{ + + MeasurementTypeInstanceReference: measurementTypeInstanceReference, + Value: entry.metricValue, + Unit: "kbit/s", + } + return meas +} diff --git a/icsversion/stub/DmaapSdnc/test-data.csv b/icsversion/stub/DmaapSdnc/test-data.csv new file mode 100644 index 0000000..6c499b1 --- /dev/null +++ b/icsversion/stub/DmaapSdnc/test-data.csv @@ -0,0 +1,10 @@ +O-DU-1122,cell-1,1,1,user-equipment-average-throughput-downlink,3761,rrm-pol-1,20,10,15 +O-DU-1122,cell-1,1,1,user-equipment-average-throughput-uplink,5861,rrm-pol-1,20,10,15 +O-DU-1122,cell-1,1,2,user-equipment-average-throughput-downlink,7791,rrm-pol-2,20,10,15 +O-DU-1122,cell-1,1,2,user-equipment-average-throughput-uplink,4539,rrm-pol-2,20,10,15 +O-DU-1122,cell-1,2,1,user-equipment-average-throughput-downlink,8987,rrm-pol-3,20,10,15 +O-DU-1122,cell-1,2,1,user-equipment-average-throughput-uplink,1134,rrm-pol-3,20,10,15 +O-DU-1122,cell-1,2,2,user-equipment-average-throughput-downlink,9123,rrm-pol-4,20,10,15 +O-DU-1122,cell-1,2,2,user-equipment-average-throughput-uplink,5368,rrm-pol-4,20,10,15 +O-DU-1122,cell-1,3,1,user-equipment-average-throughput-downlink,8764,rrm-pol-5,20,10,15 +O-DU-1122,cell-1,3,1,user-equipment-average-throughput-uplink,1367,rrm-pol-5,20,10,15 \ No newline at end of file diff --git a/icsversion/stub/ics/icsStub.go b/icsversion/stub/ics/icsStub.go new file mode 100644 index 0000000..71f33b6 --- /dev/null +++ b/icsversion/stub/ics/icsStub.go @@ -0,0 +1,80 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2022: 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 ( + "flag" + "fmt" + "net/http" + "os" + "time" + + "github.com/gorilla/mux" +) + +var client = &http.Client{ + Timeout: 5 * time.Second, +} + +func main() { + port := flag.Int("port", 8083, "The port this consumer will listen on") + flag.Parse() + fmt.Println("Starting ICS stub on port ", *port) + + r := mux.NewRouter() + r.HandleFunc("/data-consumer/v1/info-jobs/{jobId}", handleCalls).Methods(http.MethodPut, http.MethodDelete) + fmt.Println(http.ListenAndServe(fmt.Sprintf(":%v", *port), r)) +} + +func getEnv(key string, defaultVal string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + + return defaultVal +} + +func handleCalls(w http.ResponseWriter, r *http.Request) { + producer_addr := getEnv("PRODUCER_ADDR", "http://localhost:3905/") + vars := mux.Vars(r) + id, ok := vars["jobId"] + if ok { + fmt.Println(r.Method, " of job ", id) + if r.Method == http.MethodPut { + req, _ := http.NewRequest(http.MethodPut, producer_addr+"create/"+id, nil) + r, err := client.Do(req) + if err != nil { + fmt.Println("Failed to create job in producer ", err) + return + } + fmt.Println("Created job in producer ", r.Status) + } else { + req, _ := http.NewRequest(http.MethodDelete, producer_addr+"delete/"+id, nil) + r, err := client.Do(req) + if err != nil { + fmt.Println("Failed to delete job in producer ", err) + return + } + fmt.Println("Deleted job in producer ", r.Status) + } + } + +}