--- /dev/null
+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
--- /dev/null
+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=
--- /dev/null
+// -
+// ========================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
+ }
+
+}
--- /dev/null
+// -
+// ========================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!")
+ }
+ })
+ }
+}
--- /dev/null
+// -
+// ========================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)
+ }
+
+}
--- /dev/null
+// -
+// ========================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
+}
--- /dev/null
+// -
+// ========================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)
+ }
+ })
+ }
+}
--- /dev/null
+// -
+// ========================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))
+}
--- /dev/null
+// -
+// ========================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
+}
--- /dev/null
+// -
+// ========================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())
+ }
+}
--- /dev/null
+// -
+// ========================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,
+ }
+
+}
--- /dev/null
+// -
+// ========================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
+}
--- /dev/null
+// -
+// ========================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)
+
+}
--- /dev/null
+// -
+// ========================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()
+}
--- /dev/null
+// -
+// ========================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"`
+}
--- /dev/null
+// -
+// ========================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
+}
--- /dev/null
+// -
+// ========================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")
+ }
+
+ })
+ }
+}
--- /dev/null
+// -
+// ========================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
+}
--- /dev/null
+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
--- /dev/null
+// -
+// ========================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)
+ }
+ }
+
+}