Mock QoE xApp that sends a constant prediction 91/3791/8
authorLott, Christopher (cl778h) <cl778h@att.com>
Thu, 21 May 2020 20:46:44 +0000 (16:46 -0400)
committerLott, Christopher (cl778h) <cl778h@att.com>
Fri, 22 May 2020 16:50:24 +0000 (12:50 -0400)
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) <cl778h@att.com>
Change-Id: Ie6a6a851dff87c23a0bcaf23d9936e35d0cb3dd9

28 files changed:
.gitattributes [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.gitreview
.readthedocs.yaml [new file with mode: 0644]
Dockerfile [new file with mode: 0644]
Dockerfile-Unit-Test [new file with mode: 0644]
LICENSE.txt [new file with mode: 0644]
container-tag.yaml [new file with mode: 0644]
docs/_static/logo.png [new file with mode: 0644]
docs/conf.py [new file with mode: 0644]
docs/conf.yaml [new file with mode: 0644]
docs/developers-guide.rst [new file with mode: 0755]
docs/favicon.ico [new file with mode: 0644]
docs/index.rst [new file with mode: 0644]
docs/overview.rst [new file with mode: 0644]
docs/release-notes.rst [new file with mode: 0644]
docs/requirements-docs.txt [new file with mode: 0644]
qp/__init__.py [new file with mode: 0644]
qp/main.py [new file with mode: 0644]
rmr-version.yaml [new file with mode: 0644]
setup.py [new file with mode: 0644]
tests/__init__.py [new file with mode: 0644]
tests/conftest.py [new file with mode: 0644]
tests/fixtures/local.rt [new file with mode: 0644]
tests/fixtures/test_local.rt [new file with mode: 0644]
tests/test_qp.py [new file with mode: 0644]
tox.ini [new file with mode: 0644]
xapp-descriptor/config.json [new file with mode: 0644]

diff --git a/.gitattributes b/.gitattributes
new file mode 100644 (file)
index 0000000..38b6a85
--- /dev/null
@@ -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 (file)
index 0000000..59135fc
--- /dev/null
@@ -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
index 496e36e..26a5ffb 100644 (file)
@@ -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 (file)
index 0000000..3797dc8
--- /dev/null
@@ -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 (file)
index 0000000..402fab7
--- /dev/null
@@ -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 (file)
index 0000000..9f9f104
--- /dev/null
@@ -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 (file)
index 0000000..69a2cef
--- /dev/null
@@ -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 (file)
index 0000000..48c5b97
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..974c309
--- /dev/null
@@ -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 (file)
index 0000000..3e2b364
--- /dev/null
@@ -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 (executable)
index 0000000..e19c0c0
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..a640a57
--- /dev/null
@@ -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 (file)
index 0000000..736fffe
--- /dev/null
@@ -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 (file)
index 0000000..66e97b9
--- /dev/null
@@ -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 <http://keepachangelog.com/>`__
+and this project adheres to `Semantic Versioning <http://semver.org/>`__.
+
+[0.0.1] - 2020-05-21
+--------------------
+* Initial mock version (`RICAPP-107 <https://jira.o-ran-sc.org/browse/RICAPP-107>`_)
diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
new file mode 100644 (file)
index 0000000..09a0c1c
--- /dev/null
@@ -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 (file)
index 0000000..d4f2f7e
--- /dev/null
@@ -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 (file)
index 0000000..2074c19
--- /dev/null
@@ -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 (file)
index 0000000..d7b94dd
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644 (file)
index 0000000..78798c8
--- /dev/null
@@ -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 (file)
index 0000000..fe675d8
--- /dev/null
@@ -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 (file)
index 0000000..5c66798
--- /dev/null
@@ -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 (file)
index 0000000..69ca8d5
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..157951d
--- /dev/null
@@ -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": []
+        }
+}