From: ychacon Date: Mon, 21 Feb 2022 13:40:38 +0000 (+0100) Subject: Seed code for O-DU slice assurance new repo X-Git-Tag: 1.1.0~11 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=5542d26d709c996977d3992c58b273ce83e21e16;hp=71e113b81dbc68694aec281c85c4472640dc8095;p=nonrtric%2Frapp%2Fransliceassurance.git Seed code for O-DU slice assurance new repo Issue-ID: NONRTRIC-710 Change-Id: Ieb18848e70e9c8d035b4a541b2c8b8c2b1a76e3d Signed-off-by: ychacon --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5915080 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Documentation +.idea/ +.tox +docs/_build/ +.DS_STORE +.swagger* +docs/offeredapis/swagger/README.md + +# Eclipse +.checkstyle +.classpath +target/ +.sts4-cache +.project +.settings +.pydevproject +infer-out/ + +.vscode +.factorypath + +coverage.* diff --git a/docs/_static/logo.png b/docs/_static/logo.png new file mode 100644 index 0000000..c3b6ce5 Binary files /dev/null and b/docs/_static/logo.png differ diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..4ebe07d --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,13 @@ +from docs_conf.conf import * + +#branch configuration + +branch = 'latest' + +linkcheck_ignore = [ + 'http://localhost.*', + 'http://127.0.0.1.*', + 'https://gerrit.o-ran-sc.org.*', +] + +extensions = ['sphinxcontrib.redoc', 'sphinx.ext.intersphinx',] diff --git a/docs/conf.yaml b/docs/conf.yaml new file mode 100644 index 0000000..7235ba1 --- /dev/null +++ b/docs/conf.yaml @@ -0,0 +1,3 @@ +--- +project_cfg: oran +project: nonrtric diff --git a/docs/developer-guide.rst b/docs/developer-guide.rst new file mode 100644 index 0000000..87c77bf --- /dev/null +++ b/docs/developer-guide.rst @@ -0,0 +1,28 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2021 Nordix + +Developer Guide +=============== + +This document provides a quickstart for developers of the RAN Slice Assurance use case. + +Additional developer guides are available on the `O-RAN SC NONRTRIC Developer wiki `_. + +RAN Slice Assurance usecase +--------------------------- + +See the page in Wiki: `O-DU Slice Assurance usecase `_. + + +Kubernetes deployment +===================== + +Non-RT RIC can be also deployed in a Kubernetes cluster, `it/dep repository `_. +hosts deployment and integration artifacts. Instructions and helm charts to deploy the Non-RT-RIC functions in the +OSC NONRTRIC integrated test environment can be found in the *./nonrtric* directory. + +For more information on installation of NonRT-RIC in Kubernetes, see `Deploy NONRTRIC in Kubernetes `_. + +For more information see `Integration and Testing documentation on the O-RAN-SC wiki `_. + diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 0000000..00b0fd0 Binary files /dev/null and b/docs/favicon.ico differ diff --git a/docs/images/swagger.png b/docs/images/swagger.png new file mode 100644 index 0000000..f5a9e0c Binary files /dev/null and b/docs/images/swagger.png differ diff --git a/docs/images/yaml_logo.png b/docs/images/yaml_logo.png new file mode 100644 index 0000000..0492eb4 Binary files /dev/null and b/docs/images/yaml_logo.png differ diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..4873b95 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,16 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2021 Nordix + +Non-RT RIC +========== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + ./overview.rst + ./developer-guide.rst + ./release-notes.rst + +* :ref:`search` diff --git a/docs/overview.rst b/docs/overview.rst new file mode 100644 index 0000000..a56453d --- /dev/null +++ b/docs/overview.rst @@ -0,0 +1,9 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2021 Nordix + + +RAN Slice Assurance usecase +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A very simplified closed-loop rApp use case to re-prioritize a RAN slice's radio resource allocation priority if sufficient throughput cannot be maintained. Not intended to to be 'real-world'. diff --git a/docs/release-notes.rst b/docs/release-notes.rst new file mode 100644 index 0000000..ac8574c --- /dev/null +++ b/docs/release-notes.rst @@ -0,0 +1,166 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. http://creativecommons.org/licenses/by/4.0 +.. Copyright (C) 2021 Nordix + +============= +Release-Notes +============= + + +This document provides the release notes for the release of the different parts of the Non-RT RIC. + +.. contents:: + :depth: 1 + :local: + +Version history RAN Slice Assurance usecase +============================================ + ++------------+----------+------------------+-----------------+ +| **Date** | **Ver.** | **Author** | **Comment** | +| | | | | ++------------+----------+------------------+-----------------+ +| 2021-12-14 | 1.0.0 | Henrik Andersson | E Release | +| | | | Initial version | ++------------+----------+------------------+-----------------+ +| 2022-02-14 | 1.0.2 | Henrik Andersson | E Maintenance | +| | | | Release | ++------------+----------+------------------+-----------------+ + + +Release Data +============ + +Bronze +------ ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/2466f9d370214b578efedd1d3e38b1de17e6ca1c | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | Bronze | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2020-06-18 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | Improved stability | +| | | ++-----------------------------+---------------------------------------------------+ + +Bronze Maintenance +------------------ ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/5d4f252a530a0d9abbf2a363354c5e56e8f2f33e | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | Bronze | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2020-07-29 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | Introduce configuration of certificates | +| | | ++-----------------------------+---------------------------------------------------+ + +Cherry +------ ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/90ce16238dd6970153e1c0fbddb15e32c68c504f | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | Cherry | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2020-12-03 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | Introduction of Enrichment Service Coordinator | +| | and rAPP Catalogue | +| | | ++-----------------------------+---------------------------------------------------+ + +D +- ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/dd3ebfd784e96919a00ddd745826f8a8e074c66f | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | D | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2021-06-23 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | Improvements | +| | Introduction of initial version of Helm Manager | ++-----------------------------+---------------------------------------------------+ + +D Maintenance +------------- ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/973ae56894fb29a929fba9e344cae42e7607087b | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | D | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2021-08-10 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | Minor bug fixes | ++-----------------------------+---------------------------------------------------+ + +E Release +--------- ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/b472c167413a55a42fc7bfa08d2138f967a204fb | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | E | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2021-12-13 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | Improvements and renaming. | +| | Introduction of more usecase implementations. | ++-----------------------------+---------------------------------------------------+ + +E Maintenance Release +--------------------- ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/4df1f9ca4cd1ebc21e0c5ea57bcb0b7ef096d067 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | E | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2022-02-09 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | Improvements and bug fixes | +| | | ++-----------------------------+---------------------------------------------------+ + diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt new file mode 100644 index 0000000..939518d --- /dev/null +++ b/docs/requirements-docs.txt @@ -0,0 +1,6 @@ +tox +sphinx +sphinxcontrib-swaggerdoc +sphinx_bootstrap_theme +sphinxcontrib-redoc +lfdocs-conf \ No newline at end of file diff --git a/smoversion/Dockerfile b/smoversion/Dockerfile new file mode 100644 index 0000000..f462768 --- /dev/null +++ b/smoversion/Dockerfile @@ -0,0 +1,44 @@ +#================================================================================== +# Copyright (C) 2021: Nordix Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#================================================================================== +## +## Build +## +FROM nexus3.o-ran-sc.org:10001/golang:1.17-bullseye AS build + +WORKDIR /app + +COPY go.mod ./ +COPY go.sum ./ +RUN go mod download + +COPY . ./ + +RUN go build -o /oduclosedloop-sliceassurance + +## +## Deploy +## +FROM gcr.io/distroless/base-debian10 + +WORKDIR / + +## Copy from "build" stage +COPY --from=build /oduclosedloop-sliceassurance . + +USER nonroot:nonroot + +ENTRYPOINT ["/oduclosedloop-sliceassurance"] \ No newline at end of file diff --git a/smoversion/Dockerfile-simulator b/smoversion/Dockerfile-simulator new file mode 100644 index 0000000..b6b7e8f --- /dev/null +++ b/smoversion/Dockerfile-simulator @@ -0,0 +1,41 @@ +# Copyright (C) 2022 Nordix Foundation. All rights reserved. +# ======================================================================== +# 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================================================= +# + +## +## Build +## +FROM golang:1.17.1-bullseye AS build + +WORKDIR /app + +COPY . ./ + +RUN go build -o /sdnr-mr-sim ./stub/ + +## +## Deploy +## +FROM gcr.io/distroless/base-debian10 + +WORKDIR / + +## Copy from "build" stage +COPY --from=build /sdnr-mr-sim . +COPY --from=build /app/stub/test-data.csv . + +USER nonroot:nonroot + +ENTRYPOINT ["/sdnr-mr-sim"] diff --git a/smoversion/README.md b/smoversion/README.md new file mode 100644 index 0000000..f28ed4b --- /dev/null +++ b/smoversion/README.md @@ -0,0 +1,49 @@ +# O-RAN-SC Non-RealTime RIC O-DU Closed Loop Usecase Slice Assurance + +## Configuration + +The consumer takes a number of environment variables, described below, as configuration. + +>- MR_HOST **Required**. The host for Dmaap Message Router. Example: `http://mrproducer` +>- MR_PORT **Required**. The port for the Dmaap Message Router. Example: `8095` +>- SDNR_ADDR Optional. The address for SDNR. Defaults to `http://localhost:3904`. +>- SDNR_USER Optional. The user for the SDNR. Defaults to `admin`. +>- SDNR_PASSWORD Optional. The password for the SDNR user. Defaults to `Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U`. +>- LOG_LEVEL Optional. The log level, which can be `Error`, `Warn`, `Info` or `Debug`. Defaults to `Info`. +>- POLLTIME Optional. Waiting time between one pull request to Dmaap and another. Defaults to 10 sec + +## Functionality + +There is a status call provided in a REST API on port 40936. +>- /status OK + +## Development + +To make it easy to test during development of the consumer, there is a stub provided in the `stub` folder. + +This stub is used to simulate both received VES messages from Dmaap MR with information about performance measurements for the slices in a determinated DU and also SDNR, that sends information about Radio Resource Management Policy Ratio and allows to modify value for RRM Policy Dedicated Ratio from default to higher value. + +By default, SDNR stub listens to the port `3904`, but his can be overridden by passing a `--sdnr-port [PORT]` flag when starting the stub. For Dmaap MR stub default port is `3905` but it can be overriden by passing a `--dmaap-port [PORT]` flag when starting the stub. + +To build and start the stub, do the following: + +>1. cd stub +>2. go build +>3. ./stub [--sdnr-port ] [--dmaap-port ] + +## License + +Copyright (C) 2021 Nordix Foundation. +Licensed under the Apache License, Version 2.0 (the "License") +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +For more information about license please see the [LICENSE](LICENSE.txt) file for details. \ No newline at end of file diff --git a/smoversion/build-ransliceassurance-ubuntu.sh b/smoversion/build-ransliceassurance-ubuntu.sh new file mode 100755 index 0000000..8b56c2a --- /dev/null +++ b/smoversion/build-ransliceassurance-ubuntu.sh @@ -0,0 +1,42 @@ +#!/bin/bash +############################################################################## +# +# Copyright (C) 2021: Nordix Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################## +set -eux + +echo "--> build-ransliceassurance-ubuntu.sh" +curdir=`pwd` +# go installs tools like go-acc to $HOME/go/bin +# ubuntu minion path lacks go +export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin +go version +cd smoversion/ + +# install the go coverage tool helper +go get -v github.com/ory/go-acc + +export GO111MODULE=on +go get github.com/stretchr/testify/mock@v1.7.0 + +go-acc ./... --ignore mocks + +sed -i -e 's/oransc\.org\/usecase\/oduclosedloop/smoversion/' coverage.txt + +oransc.org/usecase/oduclosedloop/internal/config + +cp coverage.txt $curdir +echo "--> build-ransliceassurance-ubuntu.sh ends" diff --git a/smoversion/container-tag.yaml b/smoversion/container-tag.yaml new file mode 100644 index 0000000..f84eeb1 --- /dev/null +++ b/smoversion/container-tag.yaml @@ -0,0 +1,5 @@ +# The Jenkins job requires a tag to build the Docker image. +# By default this file is in the docker build directory, +# but the location can configured in the JJB template. +--- +tag: 1.1.0 diff --git a/smoversion/docker-compose.yaml b/smoversion/docker-compose.yaml new file mode 100644 index 0000000..2ebc1f5 --- /dev/null +++ b/smoversion/docker-compose.yaml @@ -0,0 +1,55 @@ +# Copyright (C) 2022 Nordix Foundation. All rights reserved. +# ======================================================================== +# 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================================================= +# +version: '3.5' + +networks: + default: + driver: bridge + name: nonrtric-docker-net + +services: + sdnr-mr-sim: + build: + context: . + dockerfile: Dockerfile-simulator + container_name: sdnr-mr-sim + networks: + default: + aliases: + - sdnr-mr-sim + ports: + - 3904:3904 + - 3905:3905 + + odu-app: + build: + context: . + dockerfile: Dockerfile + container_name: odu-app + networks: + default: + aliases: + - odu-app + ports: + - 8086:8086 + environment: + - MR_HOST=http://sdnr-mr-sim + - MR_PORT=3905 + - SDNR_ADDR=http://sdnr-mr-sim:3904 + - SDNR_USER=admin + - SDNR_PASSWORD=Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U + - LOG_LEVEL=Info + - POLLTIME=10 \ No newline at end of file diff --git a/smoversion/go.mod b/smoversion/go.mod new file mode 100644 index 0000000..d0c4dc2 --- /dev/null +++ b/smoversion/go.mod @@ -0,0 +1,16 @@ +module oransc.org/usecase/oduclosedloop + +go 1.17 + +require github.com/sirupsen/logrus v1.8.1 + +require ( + github.com/gorilla/mux v1.8.0 + github.com/stretchr/testify v1.3.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect +) diff --git a/smoversion/go.sum b/smoversion/go.sum new file mode 100644 index 0000000..f127d2a --- /dev/null +++ b/smoversion/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/smoversion/helm/odu-app/.helmignore b/smoversion/helm/odu-app/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/smoversion/helm/odu-app/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/smoversion/helm/odu-app/Chart.yaml b/smoversion/helm/odu-app/Chart.yaml new file mode 100644 index 0000000..56ac887 --- /dev/null +++ b/smoversion/helm/odu-app/Chart.yaml @@ -0,0 +1,39 @@ +# Copyright (C) 2021 Nordix Foundation. All rights reserved. +# ======================================================================== +# 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================================================= + +apiVersion: v1 +name: odu-app +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/smoversion/helm/odu-app/templates/_helpers.tpl b/smoversion/helm/odu-app/templates/_helpers.tpl new file mode 100644 index 0000000..0220d2e --- /dev/null +++ b/smoversion/helm/odu-app/templates/_helpers.tpl @@ -0,0 +1,79 @@ +{{/* +# Copyright (C) 2021 Nordix Foundation. All rights reserved. +# ======================================================================== +# 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================================================= +*/}} + +{{/* +Expand the name of the chart. +*/}} +{{- define "odu-app.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "odu-app.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "odu-app.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "odu-app.labels" -}} +helm.sh/chart: {{ include "odu-app.chart" . }} +{{ include "odu-app.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "odu-app.selectorLabels" -}} +app.kubernetes.io/name: {{ include "odu-app.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "odu-app.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "odu-app.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/smoversion/helm/odu-app/templates/deployment.yaml b/smoversion/helm/odu-app/templates/deployment.yaml new file mode 100644 index 0000000..d9661e4 --- /dev/null +++ b/smoversion/helm/odu-app/templates/deployment.yaml @@ -0,0 +1,72 @@ +# Copyright (C) 2021 Nordix Foundation. All rights reserved. +# ======================================================================== +# 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================================================= + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "odu-app.fullname" . }} + labels: + {{- include "odu-app.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "odu-app.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "odu-app.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: MR_HOST + value: "{{ .Values.messagerouter.host }}" + - name: MR_PORT + value: "{{ .Values.messagerouter.port }}" + - name: SDNR_ADDRESS + value: "{{ .Values.sdnr.address }}" + ports: + - name: http + containerPort: 80 + protocol: TCP + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/smoversion/helm/odu-app/templates/service.yaml b/smoversion/helm/odu-app/templates/service.yaml new file mode 100644 index 0000000..6c64790 --- /dev/null +++ b/smoversion/helm/odu-app/templates/service.yaml @@ -0,0 +1,30 @@ +# Copyright (C) 2021 Nordix Foundation. All rights reserved. +# ======================================================================== +# 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================================================= + +apiVersion: v1 +kind: Service +metadata: + name: {{ include "odu-app.fullname" . }} + labels: + {{- include "odu-app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "odu-app.selectorLabels" . | nindent 4 }} diff --git a/smoversion/helm/odu-app/values.yaml b/smoversion/helm/odu-app/values.yaml new file mode 100644 index 0000000..f36dca9 --- /dev/null +++ b/smoversion/helm/odu-app/values.yaml @@ -0,0 +1,72 @@ +# Copyright (C) 2021 Nordix Foundation. All rights reserved. +# ======================================================================== +# 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================================================= + +# Default values for odu-app. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: odu-app + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +messagerouter: + host: http://dmaap-mr + port: 3904 + +sdnr: + address: http://sdnr-simulator:9990 + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/smoversion/internal/config/config.go b/smoversion/internal/config/config.go new file mode 100644 index 0000000..f1eb26f --- /dev/null +++ b/smoversion/internal/config/config.go @@ -0,0 +1,85 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package config + +import ( + "fmt" + "os" + "strconv" + + log "github.com/sirupsen/logrus" +) + +type Config struct { + MRHost string + MRPort string + SDNRAddress string + SDNRUser string + SDNPassword string + Polltime int + LogLevel log.Level +} + +func New() *Config { + return &Config{ + MRHost: getEnv("MR_HOST", ""), + MRPort: getEnv("MR_PORT", ""), + SDNRAddress: getEnv("SDNR_ADDR", "http://localhost:3904"), + SDNRUser: getEnv("SDNR_USER", "admin"), + SDNPassword: getEnv("SDNR_PASSWORD", "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U"), + Polltime: getEnvAsInt("Polltime", 30), + LogLevel: getLogLevel(), + } +} + +func (c Config) String() string { + return fmt.Sprintf("[MRHost: %v, MRPort: %v, SDNRAddress: %v, SDNRUser: %v, SDNRPassword: %v, PollTime: %v, LogLevel: %v]", c.MRHost, c.MRPort, c.SDNRAddress, c.SDNRUser, c.SDNPassword, c.Polltime, 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/smoversion/internal/config/config_test.go b/smoversion/internal/config/config_test.go new file mode 100644 index 0000000..1005946 --- /dev/null +++ b/smoversion/internal/config/config_test.go @@ -0,0 +1,108 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package config + +import ( + "bytes" + "os" + "testing" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +func TestNewEnvVarsSetConfigContainSetValues(t *testing.T) { + assertions := require.New(t) + os.Setenv("MR_HOST", "consumerHost") + os.Setenv("MR_PORT", "8095") + os.Setenv("SDNR_ADDR", "http://localhost:3904") + os.Setenv("SDNR_USER", "admin") + os.Setenv("SDNR_PASSWORD", "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U") + os.Setenv("Polltime", "30") + os.Setenv("LOG_LEVEL", "Debug") + t.Cleanup(func() { + os.Clearenv() + }) + wantConfig := Config{ + MRHost: "consumerHost", + MRPort: "8095", + SDNRAddress: "http://localhost:3904", + SDNRUser: "admin", + SDNPassword: "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U", + Polltime: 30, + LogLevel: log.DebugLevel, + } + + got := New() + assertions.Equal(&wantConfig, got) +} + +func TestNewFaultyIntValueSetConfigContainDefaultValueAndWarnInLog(t *testing.T) { + assertions := require.New(t) + var buf bytes.Buffer + log.SetOutput(&buf) + + os.Setenv("Polltime", "wrong") + t.Cleanup(func() { + log.SetOutput(os.Stderr) + os.Clearenv() + }) + wantConfig := Config{ + MRHost: "", + MRPort: "", + SDNRAddress: "http://localhost:3904", + SDNRUser: "admin", + SDNPassword: "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U", + Polltime: 30, + LogLevel: log.InfoLevel, + } + + got := New() + assertions.Equal(&wantConfig, got) + + logString := buf.String() + assertions.Contains(logString, "Invalid int value: wrong for variable: Polltime. Default value: 30 will be used") +} + +func TestNewEnvFaultyLogLevelConfigContainDefaultValues(t *testing.T) { + assertions := require.New(t) + var buf bytes.Buffer + log.SetOutput(&buf) + + os.Setenv("LOG_LEVEL", "wrong") + t.Cleanup(func() { + log.SetOutput(os.Stderr) + os.Clearenv() + }) + wantConfig := Config{ + MRHost: "", + MRPort: "", + SDNRAddress: "http://localhost:3904", + SDNRUser: "admin", + SDNPassword: "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U", + Polltime: 30, + LogLevel: log.InfoLevel, + } + got := New() + assertions.Equal(&wantConfig, got) + logString := buf.String() + assertions.Contains(logString, "Invalid log level: wrong. Log level will be Info!") +} diff --git a/smoversion/internal/restclient/client.go b/smoversion/internal/restclient/client.go new file mode 100644 index 0000000..c31fa1d --- /dev/null +++ b/smoversion/internal/restclient/client.go @@ -0,0 +1,196 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package restclient + +import ( + "bytes" + "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{}, userName string, password string) error { + req, err := c.newRequest(http.MethodPut, path, payload, userName, password) + 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) 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/smoversion/internal/restclient/client_test.go b/smoversion/internal/restclient/client_test.go new file mode 100644 index 0000000..de66e5d --- /dev/null +++ b/smoversion/internal/restclient/client_test.go @@ -0,0 +1,220 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package restclient + +import ( + "bytes" + "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{} + } + tests := []struct { + name string + args args + want *http.Request + wantErr error + }{ + { + name: "succesfull newRequest", + args: args{ + method: http.MethodGet, + path: "url", + payload: "body", + }, + 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) + + 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")) + 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{} + } + tests := []struct { + name string + args args + wantErr string + }{ + { + name: "successful GET request", + args: args{ + header: "application/json", + respCode: http.StatusOK, + resp: "Success!", + }, + 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 res interface{} + 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) { + header := "application/json" + respCode := http.StatusOK + resp := "Success!" + + 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(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.Post(srv.URL, payload, nil, "admin", "pass") + + if err != nil { + assert.Equal(t, "", err.Error()) + } +} + +func TestPut(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.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(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.Put(srv.URL, payload, nil, "admin", "pass") + + if err != nil { + assert.Equal(t, "", err.Error()) + } +} diff --git a/smoversion/internal/sliceassurance/app.go b/smoversion/internal/sliceassurance/app.go new file mode 100644 index 0000000..c4a4d65 --- /dev/null +++ b/smoversion/internal/sliceassurance/app.go @@ -0,0 +1,152 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package sliceassurance + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "oransc.org/usecase/oduclosedloop/internal/config" + "oransc.org/usecase/oduclosedloop/internal/restclient" + "oransc.org/usecase/oduclosedloop/internal/structures" + "oransc.org/usecase/oduclosedloop/messages" + + log "github.com/sirupsen/logrus" +) + +const ( + THRESHOLD_TPUT = 7000 + DEFAULT_DEDICATED_RATIO = 15 + NEW_DEDICATED_RATIO = 25 + NODE_ID = "O-DU-1122" +) + +type App struct { + client *restclient.Client + metricsPolicies *structures.SliceAssuranceMeas +} + +var dmaapMRUrl string +var sDNRUrl string +var sDNRUsername string +var sDNRPassword string + +func (a *App) Initialize(config *config.Config) { + dmaapMRUrl = config.MRHost + ":" + config.MRPort + sDNRUrl = config.SDNRAddress + sDNRUsername = config.SDNRUser + sDNRPassword = config.SDNPassword + + a.client = restclient.New(&http.Client{}, false) + a.metricsPolicies = structures.NewSliceAssuranceMeas() +} + +func (a *App) Run(topic string, pollTime int) { + for { + a.getMessagesFromDmaap(dmaapMRUrl + topic) + + for key := range a.metricsPolicies.Metrics { + a.getRRMInformation(key.Duid) + } + a.updateDedicatedRatio() + + time.Sleep(time.Second * time.Duration(pollTime)) + } +} + +func (a *App) getMessagesFromDmaap(path string) { + log.Infof("Polling new messages from DmaapMR %v", path) + + //Added to work with onap-Dmaap + var messageStrings []string + if error := a.client.Get(path, &messageStrings); error != nil { + log.Warn("Send of Get messages from DmaapMR failed! ", error) + } + + for _, msgString := range messageStrings { + var message messages.StdDefinedMessage + if err := json.Unmarshal([]byte(msgString), &message); err == nil { + 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 := a.metricsPolicies.AddOrUpdateMetric(meas); err != nil { + log.Error("Metric could not be added ", err) + } + } + } else { + log.Warn(err) + } + } +} + +func (a *App) getRRMInformation(duid string) { + var duRRMPolicyRatio messages.ORanDuRestConf + + log.Infof("Get RRM Information from SDNR url: %v", sDNRUrl) + if error := a.client.Get(getUrlForDistributedUnitFunctions(sDNRUrl, duid), &duRRMPolicyRatio, sDNRUsername, 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) + a.metricsPolicies.AddNewPolicy(duid, policy) + } + } +} + +func (a *App) updateDedicatedRatio() { + for _, metric := range a.metricsPolicies.Metrics { + policy, check := a.metricsPolicies.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(sDNRUrl, metric.DUId, policy.PolicyRatioId) + updatePolicyMessage := policy.GetUpdateDedicatedRatioMessage(metric.SliceDiff, metric.SliceServiceType, NEW_DEDICATED_RATIO) + prettyPrint(updatePolicyMessage) + if error := a.client.Put(path, updatePolicyMessage, nil, sDNRUsername, 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/smoversion/internal/structures/measurements.go b/smoversion/internal/structures/measurements.go new file mode 100644 index 0000000..7842916 --- /dev/null +++ b/smoversion/internal/structures/measurements.go @@ -0,0 +1,88 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package structures + +import "oransc.org/usecase/oduclosedloop/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/smoversion/internal/structures/sliceassurance.go b/smoversion/internal/structures/sliceassurance.go new file mode 100644 index 0000000..d7c2556 --- /dev/null +++ b/smoversion/internal/structures/sliceassurance.go @@ -0,0 +1,144 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package structures + +import ( + "fmt" + "regexp" + "strconv" + + log "github.com/sirupsen/logrus" + "oransc.org/usecase/oduclosedloop/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 +} + +func (sa *SliceAssuranceMeas) PrintStructures() { + fmt.Printf("SliceAssurance Metrics: \n") + for key, metric := range sa.Metrics { + fmt.Printf("Key: %+v\n", key) + fmt.Printf("Metric: %+v\n", metric) + } + fmt.Printf("SliceAssurance Policies: \n") + for key, metric := range sa.Policies { + fmt.Printf("Key: %+v\n", key) + fmt.Printf("Metric: %+v\n", metric) + } +} diff --git a/smoversion/internal/structures/sliceassurance_test.go b/smoversion/internal/structures/sliceassurance_test.go new file mode 100644 index 0000000..9ed35fb --- /dev/null +++ b/smoversion/internal/structures/sliceassurance_test.go @@ -0,0 +1,172 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package structures + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "oransc.org/usecase/oduclosedloop/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) + + sliceAssuranceMeas.PrintStructures() +} diff --git a/smoversion/main.go b/smoversion/main.go new file mode 100644 index 0000000..337e4e0 --- /dev/null +++ b/smoversion/main.go @@ -0,0 +1,66 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package main + +import ( + "fmt" + "net/http" + + log "github.com/sirupsen/logrus" + "oransc.org/usecase/oduclosedloop/internal/config" + "oransc.org/usecase/oduclosedloop/internal/sliceassurance" +) + +const TOPIC string = "/events/unauthenticated.VES_O_RAN_SC_HELLO_WORLD_PM_STREAMING_OUTPUT/myG/C1" + +var configuration *config.Config + +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.Run(TOPIC, configuration.Polltime) + + http.HandleFunc("/status", statusHandler) + + log.Fatal(http.ListenAndServe(":40936", nil)) +} + +func validateConfiguration(configuration *config.Config) error { + if configuration.MRHost == "" || configuration.MRPort == "" { + return fmt.Errorf("message router host and port must be provided") + } + return nil +} + +func statusHandler(w http.ResponseWriter, r *http.Request) { + // Just respond OK to show the service is alive for now. Might be extended later. +} diff --git a/smoversion/messages/policyRatio.go b/smoversion/messages/policyRatio.go new file mode 100644 index 0000000..eeba22e --- /dev/null +++ b/smoversion/messages/policyRatio.go @@ -0,0 +1,105 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package 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/smoversion/messages/stdVesMessage.go b/smoversion/messages/stdVesMessage.go new file mode 100644 index 0000000..5c4a3b0 --- /dev/null +++ b/smoversion/messages/stdVesMessage.go @@ -0,0 +1,78 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package 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/smoversion/messages/stdVesMessage_test.go b/smoversion/messages/stdVesMessage_test.go new file mode 100644 index 0000000..3524ba1 --- /dev/null +++ b/smoversion/messages/stdVesMessage_test.go @@ -0,0 +1,86 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package 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/smoversion/stub/Dockerfile b/smoversion/stub/Dockerfile new file mode 100644 index 0000000..5900390 --- /dev/null +++ b/smoversion/stub/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.15.2-alpine3.12 as build +RUN apk add git +RUN mkdir /build +ADD . /build +WORKDIR /build +RUN go build -o simulator . + +FROM alpine:latest +RUN mkdir /app +WORKDIR /app/ + +# Copy the Pre-built binary file from the previous stage +COPY --from=build /build . + +# Expose port 8080 +EXPOSE 8080 + +# Run Executable +ENTRYPOINT ["/stub"] \ No newline at end of file diff --git a/smoversion/stub/simulator.go b/smoversion/stub/simulator.go new file mode 100644 index 0000000..aef85d8 --- /dev/null +++ b/smoversion/stub/simulator.go @@ -0,0 +1,377 @@ +// - +// ========================LICENSE_START================================= +// O-RAN-SC +// %% +// Copyright (C) 2021: Nordix Foundation +// %% +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================LICENSE_END=================================== +// + +package main + +import ( + "encoding/csv" + "encoding/json" + "flag" + "fmt" + "math/rand" + "net/http" + "os" + "strconv" + "sync" + "time" + + "github.com/gorilla/mux" + "oransc.org/usecase/oduclosedloop/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 + +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") + portDmaapMR := flag.Int("dmaap-port", 3905, "The port this Dmaap message router 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("/events/unauthenticated.VES_O_RAN_SC_HELLO_WORLD_PM_STREAMING_OUTPUT/myG/C1", sendDmaapMRMessages).Methods(http.MethodGet) + + fmt.Println("Starting DmaapMR stub on port: ", *portDmaapMR) + + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", *portDmaapMR), r)) + wg.Done() + }() + + wg.Wait() +} + +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 + keys := make(map[string]bool) + for _, entry := range data { + if _, value := keys[entry.policyRatioId]; !value { + keys[entry.policyRatioId] = true + 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 sendDmaapMRMessages(w http.ResponseWriter, r *http.Request) { + log.Info("Send Dmaap messages") + 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 + } + randomEventId := rand.Intn(10000) + messagesToSend = append(messagesToSend, generateMeasurementEntry(entry)) + + message := messages.StdDefinedMessage{ + Event: messages.Event{ + CommonEventHeader: messages.CommonEventHeader{ + Domain: "stndDefined", + EventId: "pm-1_16442" + strconv.Itoa(randomEventId), + 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, + }, + }, + }, + } + + fmt.Printf("Sending Dmaap message:\n %+v\n", message) + + messageAsByteArray, _ := json.Marshal(message) + response := [1]string{string(messageAsByteArray)} + + time.Sleep(time.Duration(rand.Intn(3)) * time.Second) + respondWithJSON(w, http.StatusOK, response) + + messagesToSend = nil +} + +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/smoversion/stub/test-data.csv b/smoversion/stub/test-data.csv new file mode 100644 index 0000000..6c499b1 --- /dev/null +++ b/smoversion/stub/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/tox.ini b/tox.ini new file mode 100644 index 0000000..2705e16 --- /dev/null +++ b/tox.ini @@ -0,0 +1,37 @@ +# ================================================================================== +# Copyright (c) 2020 Nordix +# +# 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. +# ================================================================================== + +# documentation only +[tox] +minversion = 2.0 +envlist = + docs, + docs-linkcheck, +skipsdist = true + +[testenv:docs] +basepython = python3 +deps = -r{toxinidir}/docs/requirements-docs.txt + +commands = + sphinx-build -W -b html -n -d {envtmpdir}/docs/doctrees ./docs/ {toxinidir}/docs/_build/html + echo "Generated docs available in {toxinidir}/docs/_build/html" +whitelist_externals = echo + +[testenv:docs-linkcheck] +basepython = python3 +deps = -r{toxinidir}/docs/requirements-docs.txt +commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/linkcheck