From: Lott, Christopher (cl778h) Date: Thu, 21 May 2020 20:46:44 +0000 (-0400) Subject: Mock QoE xApp that sends a constant prediction X-Git-Tag: 0.0.2~1 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=a7b3ffdee8ce23785bf71b9a28d480f8130ca937;p=ric-app%2Fqp.git Mock QoE xApp that sends a constant prediction Version 0.0.1 of an xApplication based on the Python xApp framework. This mockup listens for a QoE prediction request (RMR message type 30001) as sent by the QP Driver xApp. Reacts by sending a fixed QoE prediction (RMR message type 30002), which should be routed to the Traffic Steering xApp. Includes a static RMR routing table with this entry for TS: rte|30002|service-ricxapp-trafficxapp-rmr.ricxapp.svc.cluster.local:4560 Issue-ID: RICAPP-107 Signed-off-by: Lott, Christopher (cl778h) Change-Id: Ie6a6a851dff87c23a0bcaf23d9936e35d0cb3dd9 --- diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..38b6a85 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,21 @@ +# https://help.github.com/articles/dealing-with-line-endings/ + +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized +# and converted to native line endings on checkout. +*.css text +*.htm text diff=html +*.html text diff=html +*.java text diff=java +*.js text +*.jsp text +*.less text +*.properties text +*.sql text +*.xml text + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59135fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +# misc cruft +*.log +log.txt +rmr/* +docs_and_diagrams/ + +# documentation +.tox +docs/_build/ + +# standard python ignore template +.pytest_cache/ +xunit-results.xml +.DS_Store +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +venv-tox/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# Test report +xunit-reports +coverage-reports + +# Eclipse +.project +.pydevproject +.settings diff --git a/.gitreview b/.gitreview index 496e36e..26a5ffb 100644 --- a/.gitreview +++ b/.gitreview @@ -1,7 +1,6 @@ - - [gerrit] - host=gerrit.o-ran-sc.org - port=29418 - project=ric-app/qp - defaultbranch=master - \ No newline at end of file +[gerrit] +host=gerrit.o-ran-sc.org +port=29418 +project=ric-app/qp +defaultbranch=master +defaultremote=origin diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..3797dc8 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,20 @@ +--- +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +# Required +version: 2 + +formats: + - htmlzip + +build: + image: latest + +python: + version: 3.7 + install: + - requirements: docs/requirements-docs.txt + +sphinx: + configuration: docs/conf.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..402fab7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# ================================================================================== +# Copyright (c) 2020 AT&T Intellectual Property. +# +# 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. +# ================================================================================== +FROM python:3.8-alpine + +# RMR setup +RUN mkdir -p /opt/route/ +# copy rmr files from builder image in lieu of an Alpine package +COPY --from=nexus3.o-ran-sc.org:10002/o-ran-sc/bldr-alpine3-rmr:4.0.5 /usr/local/lib64/librmr* /usr/local/lib64/ +# rmr_probe replaced health_ck +COPY --from=nexus3.o-ran-sc.org:10002/o-ran-sc/bldr-alpine3-rmr:4.0.5 /usr/local/bin/rmr* /usr/local/bin/ +ENV LD_LIBRARY_PATH /usr/local/lib/:/usr/local/lib64 +COPY tests/fixtures/local.rt /opt/route/local.rt +ENV RMR_SEED_RT /opt/route/local.rt + +# sdl needs gcc +RUN apk update && apk add gcc musl-dev + +# Install +COPY setup.py /tmp +COPY LICENSE.txt /tmp/ +COPY qp/ /tmp/qp +RUN pip install /tmp + +# Run +ENV PYTHONUNBUFFERED 1 +CMD run-qp.py diff --git a/Dockerfile-Unit-Test b/Dockerfile-Unit-Test new file mode 100644 index 0000000..9f9f104 --- /dev/null +++ b/Dockerfile-Unit-Test @@ -0,0 +1,35 @@ +# ================================================================================== +# Copyright (c) 2020 AT&T Intellectual Property. +# +# 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. +# ================================================================================== +FROM python:3.8-alpine + +# sdl uses hiredis which needs gcc +RUN apk update && apk add gcc musl-dev + +# copy rmr libraries from builder image in lieu of an Alpine package +COPY --from=nexus3.o-ran-sc.org:10002/o-ran-sc/bldr-alpine3-rmr:4.0.5 /usr/local/lib64/librmr* /usr/local/lib64/ + +# Upgrade pip, install tox +RUN pip install --upgrade pip && pip install tox + +# copies +COPY setup.py tox.ini LICENSE.txt /tmp/ +COPY qp/ /tmp/qp +COPY tests/ /tmp/tests +RUN pip install /tmp + +# Run the unit tests +WORKDIR /tmp +RUN tox -e code,flake8 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..69a2cef --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,29 @@ + + Unless otherwise specified, all software contained herein is licensed + under the Apache License, Version 2.0 (the "Software License"); + you may not use this software except in compliance with the Software + License. You may obtain a copy of the Software License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the Software License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the Software License for the specific language governing permissions + and limitations under the Software License. + + + + Unless otherwise specified, all documentation contained herein is licensed + under the Creative Commons License, Attribution 4.0 Intl. (the + "Documentation License"); you may not use this documentation except in + compliance with the Documentation License. You may obtain a copy of the + Documentation License at + + https://creativecommons.org/licenses/by/4.0/ + + Unless required by applicable law or agreed to in writing, documentation + distributed under the Documentation License is distributed on an "AS IS" + BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the Documentation License for the specific language governing + permissions and limitations under the Documentation License. diff --git a/container-tag.yaml b/container-tag.yaml new file mode 100644 index 0000000..48c5b97 --- /dev/null +++ b/container-tag.yaml @@ -0,0 +1,4 @@ +# The Jenkins job uses this string for the tag in the image name +# for example nexus3.o-ran-sc.org:10004/my-image-name:my-tag +--- +tag: 0.0.1 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..974c309 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,3 @@ +from docs_conf.conf import * + +linkcheck_ignore = ["http://localhost.*", "http://127.0.0.1.*", "https://gerrit.o-ran-sc.org.*"] diff --git a/docs/conf.yaml b/docs/conf.yaml new file mode 100644 index 0000000..3e2b364 --- /dev/null +++ b/docs/conf.yaml @@ -0,0 +1,3 @@ +--- +project_cfg: oran +project: ric-app-qp diff --git a/docs/developers-guide.rst b/docs/developers-guide.rst new file mode 100755 index 0000000..e19c0c0 --- /dev/null +++ b/docs/developers-guide.rst @@ -0,0 +1,56 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2020 AT&T Intellectual Property + + +Developers Guide +================= + +.. contents:: + :depth: 3 + :local: + + +Version bumping the Xapp +------------------------ + +This project follows semver. When changes are made, update the version strings in: + +#. ``container-tag.yaml`` +#. ``docs/release-notes.rst`` +#. ``setup.py`` + + +Testing RMR Healthcheck +----------------------- +The following instructions should deploy the QP container in bare docker, and allow you +to test that the RMR healthcheck is working. + +:: + + docker build -t qpd:latest -f Dockerfile . + docker run -d --net=host -e USE_FAKE_SDL=1 qpd:latest + docker exec -it CONTAINER_ID /usr/local/bin/rmr_probe -h 127.0.0.1:4562 + +Unit Testing +------------ + +Running the unit tests requires the python packages ``tox`` and ``pytest``. + +The RMR library is also required during unit tests. If running directly from tox +(outside a Docker container), install RMR according to its instructions. + +Upon completion, view the test coverage like this: + +:: + + tox + open htmlcov/index.html + +Alternatively, if you cannot install RMR locally, you can run the unit +tests in Docker. This is somewhat less nice because you don't get the +pretty HTML report on coverage. + +:: + + docker build --no-cache -f Dockerfile-Unit-Test . 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/index.rst b/docs/index.rst new file mode 100644 index 0000000..a640a57 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,19 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2020 AT&T Intellectual Property + + +QoE Predictor xApp +================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + overview.rst + developers-guide.rst + release-notes.rst + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/overview.rst b/docs/overview.rst new file mode 100644 index 0000000..736fffe --- /dev/null +++ b/docs/overview.rst @@ -0,0 +1,109 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2020 AT&T Intellectual Property + +QoE Predictor Overview +====================== + +QoE Predictor (QP) is an Xapp in the Traffic Steering O-RAN use case, +which uses the following Xapps: + +#. Traffic Steering, which sends prediction requests to QP Driver. +#. QP Driver, which fetches data from SDL on behalf of traffic steering, + both UE Data and Cell Data, merges that data together, then sends off + the data to the QoE Predictor. +#. QoE Predictor, which predicts and sends that prediction back to Traffic Steering +#. KPIMONN, which populates SDL in the first place. + +Expected Input +-------------- + +The QP Xapp expects a prediction-request JSON message via RMR with the following structure:: + + { + "predictionUE": "UEId1", + "ueMeasurements" : + { "servingCellId" : "CID2", + "measTimestampUePrbUsage" : TS1, + "measPeriodUePrbUsage" : Int, + "uePrbUsageDL" : Int, + "uePrbUsageUL" : Int, + "measTimestampUePdcpBytes" : TS2, + "measPeriodUePdcpByes" : Int, + "uePdcpBytesDL": Int, + "uePdcpBytesUL" : Int + }, + "cellMeasurements" : [ + { + "cellId" : "CID2", + "measTimestampPrbAvailable" : TS, + "measPeriodPrbAvailable" : Int, + "prbAvailableDL" : Int, + "prbAvailableUL" : Int, + "measTimestampPdcpBytes" : TS, + "measPeriodPdcpBytes" : Int, + "pdcpBytesDL" : 30000000, + "pdcpBytesUL" : 5000000, + "measTimestampRf" : TS, + "measPeriodRf" : Int, + "rfMeasurements" : { + "rsrp": Int, + "rsrq": Int, + "rsSinr": Int + } + }, + { + "cellId" : "CID1", + "measTimestampPrbAvailable" : TS, + "measPeriodPrbAvailable" : Int, + "prbAvailableDL" : Int, + "prbAvailableUL" : Int, + "measTimestampPdcpBytes" : TS, + "measPeriodPdcpBytes" : Int, + "pdcpBytesDL" : 10000000, + "pdcpBytesUL" : 2000000, + "measTimestampRf" : TS, + "measPeriodRf" : Int, + "rfMeasurements" : { + "rsrp": Int, + "rsrq": Int, + "rsSinr": Int + } + }, + { + "cellId" : "CID3", + "measTimestampPrbAvailable" : TS, + "measPeriodPrbAvailable" : Int, + "prbAvailableDL" : Int, + "prbAvailableUL" : Int, + "measTimestampPdcpBytes" : TS, + "measPeriodPdcpBytes" : Int, + "pdcpBytesDL" : 50000000, + "pdcpBytesUL" : 4000000, + "measTimestampRf" : TS, + "measPeriodRf" : Int, + "rfMeasurements" : { + "rsrp": Int, + "rsrq": Int, + "rsSinr": Int + } + } + ] + } + + +Expected Output +--------------- + +The QP Xapp should send a prediction for both downlink and uplink throughput +as a JSON message via RMR with the following structure:: + + { + "UEId1": { + "CID1" : [10000000,2000000], + "CID2" : [30000000,5000000], + "CID3" : [50000000,4000000] + } + } + + diff --git a/docs/release-notes.rst b/docs/release-notes.rst new file mode 100644 index 0000000..66e97b9 --- /dev/null +++ b/docs/release-notes.rst @@ -0,0 +1,15 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2020 AT&T Intellectual Property + +Release Notes +=============== + +All notable changes to this project will be documented in this file. + +The format is based on `Keep a Changelog `__ +and this project adheres to `Semantic Versioning `__. + +[0.0.1] - 2020-05-21 +-------------------- +* Initial mock version (`RICAPP-107 `_) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt new file mode 100644 index 0000000..09a0c1c --- /dev/null +++ b/docs/requirements-docs.txt @@ -0,0 +1,5 @@ +sphinx +sphinx-rtd-theme +sphinxcontrib-httpdomain +recommonmark +lfdocs-conf diff --git a/qp/__init__.py b/qp/__init__.py new file mode 100644 index 0000000..d4f2f7e --- /dev/null +++ b/qp/__init__.py @@ -0,0 +1,15 @@ +# ================================================================================== +# Copyright (c) 2020 AT&T Intellectual Property. +# +# 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. +# ================================================================================== diff --git a/qp/main.py b/qp/main.py new file mode 100644 index 0000000..2074c19 --- /dev/null +++ b/qp/main.py @@ -0,0 +1,86 @@ +# ================================================================================== +# Copyright (c) 2020 AT&T Intellectual Property. +# +# 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. +# ================================================================================== +""" +mock qp module + +RMR Messages: + #define TS_QOE_PRED_REQ 30001 + #define TS_QOE_PREDICTION 30002 +30001 is the message type QP receives from the driver; +sends out type 30002 which should be routed to TS. + +""" + + +import os +from mdclogpy import Logger +from ricxappframe.xapp_frame import RMRXapp, rmr + + +qp_xapp = None +logger = Logger(name=__name__) + + +def post_init(self): + self.predict_requests = 0 + logger.debug("QP xApp started") + + +def qp_default_handler(self, summary, sbuf): + logger.debug("default handler received message type {}".format(summary[rmr.RMR_MS_MSG_TYPE])) + # we don't use rts here; free this + self.rmr_free(sbuf) + + +def qp_predict_handler(self, summary, sbuf): + logger.debug("predict handler received message type {}".format(summary[rmr.RMR_MS_MSG_TYPE])) + self.predict_requests += 1 + # we don't use rts here; free this + self.rmr_free(sbuf) + # send a mock message + mock_msg = '{ "12345" : { "310-680-200-555001" : [ 2000000 , 1200000 ] , "310-680-200-555002" : [ 800000 , 400000 ] , "310-680-200-555003" : [ 800000 , 400000 ] } }' + ok = self.rmr_send(mock_msg.encode(), 30002) + if ok: + logger.debug("predict handler: sent message successfully") + else: + logger.warn("predict handler: failed to send message") + + +def start(thread=False): + """ + This is a convenience function that allows this xapp to run in Docker + for "real" (no thread, real SDL), but also easily modified for unit testing + (e.g., use_fake_sdl). The defaults for this function are for the Dockerized xapp. + """ + logger.debug("QP xApp starting") + global qp_xapp + fake_sdl = os.environ.get("USE_FAKE_SDL", None) + qp_xapp = RMRXapp(qp_default_handler, post_init=post_init, use_fake_sdl=True if fake_sdl else False) + qp_xapp.register_callback(qp_predict_handler, 30001) + qp_xapp.run(thread) + + +def stop(): + """ + can only be called if thread=True when started + TODO: could we register a signal handler for Docker SIGTERM that calls this? + """ + qp_xapp.stop() + + +def get_stats(): + # hacky for now, will evolve + return {"PredictRequests": qp_xapp.predict_requests} diff --git a/rmr-version.yaml b/rmr-version.yaml new file mode 100644 index 0000000..d7b94dd --- /dev/null +++ b/rmr-version.yaml @@ -0,0 +1,3 @@ +# CI script installs RMR from PackageCloud using this version +--- +version: 4.0.5 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..8c7d905 --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +# ================================================================================== +# Copyright (c) 2020 AT&T Intellectual Property. +# +# 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. +# ================================================================================== +from setuptools import setup, find_packages + +setup( + name="qp", + version="0.0.1", + packages=find_packages(exclude=["tests.*", "tests"]), + description="Quality-of-Service Predictor Xapp for Traffic Steering", + url="https://gerrit.o-ran-sc.org/r/admin/repos/ric-app/qp", + install_requires=["ricxappframe>=1.1.1,<2.0.0"], + entry_points={"console_scripts": ["run-qp.py=qp.main:start"]}, # adds a magical entrypoint for Docker + license="Apache 2.0", + data_files=[("", ["LICENSE.txt"])], +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..78798c8 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,91 @@ +# ================================================================================== +# Copyright (c) 2020 AT&T Intellectual Property. +# +# 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. +# ================================================================================== + + +import pytest + + +@pytest.fixture +def qp_prediction(): + return { + "12345": { + "310-680-200-555001": [2000000, 1200000], + "310-680-200-555002": [800000, 400000], + "310-680-200-555003": [800000, 400000] + } + } + + +@pytest.fixture +def qpd_to_qp(): + return { + "PredictionUE": "12345", + "UEMeasurements": { + "ServingCellID": "310-680-200-555002", + "MeasTimestampUEPDCPBytes": "2020-03-18 02:23:18.220", + "MeasPeriodUEPDCPBytes": 20, + "UEPDCPBytesDL": 250000, + "UEPDCPBytesUL": 100000, + "MeasTimestampUEPRBUsage": "2020-03-18 02:23:18.220", + "MeasPeriodUEPRBUsage": 20, + "UEPRBUsageDL": 10, + "UEPRBUsageUL": 30, + }, + "CellMeasurements": [ + { + "CellID": "310-680-200-555001", + "MeasTimestampPDCPBytes": "2020-03-18 02:23:18.220", + "MeasPeriodPDCPBytes": 20, + "PDCPBytesDL": 2000000, + "PDCPBytesUL": 1200000, + "MeasTimestampAvailPRB": "2020-03-18 02:23:18.220", + "MeasPeriodAvailPRB": 20, + "AvailPRBDL": 30, + "AvailPRBUL": 50, + "MeasTimestampRF": "2020-03-18 02:23:18.210", + "MeasPeriodRF": 40, + "RFMeasurements": {"RSRP": -90, "RSRQ": -13, "RSSINR": -2.5}, + }, + { + "CellID": "310-680-200-555003", + "MeasTimestampPDCPBytes": "2020-03-18 02:23:18.220", + "MeasPeriodPDCPBytes": 20, + "PDCPBytesDL": 1900000, + "PDCPBytesUL": 1000000, + "MeasTimestampAvailPRB": "2020-03-18 02:23:18.220", + "MeasPeriodAvailPRB": 20, + "AvailPRBDL": 60, + "AvailPRBUL": 80, + "MeasTimestampRF": "2020-03-18 02:23:18.210", + "MeasPeriodRF": 40, + "RFMeasurements": {"RSRP": -140, "RSRQ": -17, "RSSINR": -6}, + }, + { + "CellID": "310-680-200-555002", + "MeasTimestampPDCPBytes": "2020-03-18 02:23:18.220", + "MeasPeriodPDCPBytes": 20, + "PDCPBytesDL": 800000, + "PDCPBytesUL": 400000, + "MeasTimestampAvailPRB": "2020-03-18 02:23:18.220", + "MeasPeriodAvailPRB": 20, + "AvailPRBDL": 30, + "AvailPRBUL": 45, + "MeasTimestampRF": "2020-03-18 02:23:18.210", + "MeasPeriodRF": 40, + "RFMeasurements": {"RSRP": -115, "RSRQ": -16, "RSSINR": -5}, + }, + ], + } diff --git a/tests/fixtures/local.rt b/tests/fixtures/local.rt new file mode 100644 index 0000000..fe675d8 --- /dev/null +++ b/tests/fixtures/local.rt @@ -0,0 +1,4 @@ +# static route table to direct messages sent by mock QP xApp +newrt|start +rte|30002|service-ricxapp-trafficxapp-rmr.ricxapp.svc.cluster.local:4560 +newrt|end diff --git a/tests/fixtures/test_local.rt b/tests/fixtures/test_local.rt new file mode 100644 index 0000000..5c66798 --- /dev/null +++ b/tests/fixtures/test_local.rt @@ -0,0 +1,6 @@ +# do NOT use localhost, seems unresolved on jenkins VMs +newrt|start +mse| 30001 | -1 | 127.0.0.1:4562 # prediction request from QPD to QP +mse| 60001 | -1 | 127.0.0.1:4562 # other message from QPD to QP +mse| 30002 | -1 | 127.0.0.1:4563 # prediction response from QP to TS +newrt|end diff --git a/tests/test_qp.py b/tests/test_qp.py new file mode 100644 index 0000000..69ca8d5 --- /dev/null +++ b/tests/test_qp.py @@ -0,0 +1,104 @@ +# ================================================================================== +# Copyright (c) 2020 AT&T Intellectual Property. +# +# 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. +# ================================================================================== +import json +import time +from contextlib import suppress +from qp import main +from ricxappframe.xapp_frame import Xapp, RMRXapp + +mock_qpd_xapp = None +mock_ts_xapp = None + +""" + these tests are not currently parallelizable (do not use this tox flag) + I would use setup_module, however that can't take monkeypatch fixtures + Currently looking for the best way to make this better: + https://stackoverflow.com/questions/60886013/python-monkeypatch-in-pytest-setup-module +""" + + +def test_init_xapp(monkeypatch): + + # monkeypatch post_init to set the data we want + def fake_post_init(self): + self.predict_requests = 0 + + # patch + monkeypatch.setattr("qp.main.post_init", fake_post_init) + + # start qp + main.start(thread=True) + + +def test_rmr_flow(monkeypatch, qpd_to_qp, qp_prediction): + """ + this flow mocks out the xapps on both sides of QP. + It first stands up a mock ts, then it starts up a mock qp-driver + which will immediately send requests to the running qp. + """ + + expected_result = {} + + # define a mock traffic steering xapp + def mock_ts_default_handler(self, summary, sbuf): + pass + + def mock_ts_prediction_handler(self, summary, sbuf): + nonlocal expected_result # closures ftw + pay = json.loads(summary["payload"]) + expected_result = pay + + global mock_ts_xapp + mock_ts_xapp = RMRXapp(mock_ts_default_handler, rmr_port=4563, use_fake_sdl=True) + mock_ts_xapp.register_callback(mock_ts_prediction_handler, 30002) + mock_ts_xapp.run(thread=True) + + time.sleep(1) + + # define a mock qp driver xapp that sends a message to QP under test + def mock_qpd_entry(self): + + # good traffic steering request + val = json.dumps(qpd_to_qp).encode() + self.rmr_send(val, 30001) + + # should trigger the default handler and do nothing + val = json.dumps({"test send 60001": 2}).encode() + self.rmr_send(val, 60001) + + global mock_qpd_xapp + mock_qpd_xapp = Xapp(entrypoint=mock_qpd_entry, rmr_port=4666, use_fake_sdl=True) + mock_qpd_xapp.run() # this will return since entry isn't a loop + + time.sleep(1) + + assert main.get_stats() == {"PredictRequests": 1} + assert expected_result == qp_prediction + + +def teardown_module(): + """ + this is like a "finally"; the name of this function is pytest magic + safer to put down here since certain failures above can lead to pytest never returning + for example if an exception gets raised before stop is called in any test function above, + pytest will hang forever + """ + with suppress(Exception): + mock_ts_xapp.stop() + with suppress(Exception): + mock_qpd_xapp.stop() + with suppress(Exception): + main.stop() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..18bedff --- /dev/null +++ b/tox.ini @@ -0,0 +1,73 @@ +# ================================================================================== +# Copyright (c) 2020 AT&T Intellectual Property. +# +# 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. +# ================================================================================== +[tox] +envlist = code,flake8,docs,docs-linkcheck +minversion = 2.0 + +[testenv:code] +basepython = python3.8 +deps= + pytest + coverage + pytest-cov +setenv = + LD_LIBRARY_PATH = /usr/local/lib/:/usr/local/lib64 + RMR_SEED_RT = tests/fixtures/test_local.rt + RMR_ASYNC_CONN = 0 + USE_FAKE_SDL = 1 + +commands = + pytest -v --cov qp --cov-report xml --cov-report term-missing --cov-report html --cov-fail-under=70 + coverage xml -i + +[testenv:flake8] +basepython = python3.8 +skip_install = true +deps = flake8 +commands = flake8 setup.py qp tests + +[flake8] +extend-ignore = E501,E741,E731 + +[testenv:clm] +# use pip to gather dependencies with versions for CLM analysis +whitelist_externals = sh +commands = sh -c 'pip freeze > requirements.txt' + +# doc jobs +[testenv:docs] +whitelist_externals = echo +skipsdist = true +basepython = python3.8 +deps = + sphinx + sphinx-rtd-theme + sphinxcontrib-httpdomain + recommonmark + lfdocs-conf +commands = + sphinx-build -W -b html -n -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/html + echo "Generated docs available in {toxinidir}/docs/_build/html" + +[testenv:docs-linkcheck] +skipsdist = true +basepython = python3.8 +deps = sphinx + sphinx-rtd-theme + sphinxcontrib-httpdomain + recommonmark + lfdocs-conf +commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/linkcheck diff --git a/xapp-descriptor/config.json b/xapp-descriptor/config.json new file mode 100644 index 0000000..157951d --- /dev/null +++ b/xapp-descriptor/config.json @@ -0,0 +1,41 @@ +{ + "xapp_name": "qp", + "version": "0.0.1", + "containers": [ + { + "name": "qp", + "image": { + "registry": "nexus3.o-ran-sc.org:10002", + "name": "o-ran-sc/ric-app-qp", + "tag": "0.0.1" + } + } + ], + "messaging": { + "ports": [ + { + "name": "rmr-data-in", + "container": "qp", + "port": 4562, + "rxMessages": ["TS_QOE_PRED_REQ"], + "txMessages": ["TS_QOE_PREDICTION"], + "policies": [], + "description": "rmr receive data port for qp" + }, + { + "name": "rmr-route", + "container": "qp", + "port": 4561, + "description": "rmr route port for qp" + } + ] + }, + "rmr": { + "protPort": "tcp:4562", + "maxSize": 2072, + "numWorkers": 1, + "rxMessages": ["TS_QOE_PRED_REQ"], + "txMessages": ["TS_QOE_PREDICTION"], + "policies": [] + } +}