Initial commit of A1 17/217/1
authorTommy Carpenter <tommy@research.att.com>
Thu, 30 May 2019 18:33:21 +0000 (14:33 -0400)
committerTommy Carpenter <tommy@research.att.com>
Thu, 30 May 2019 18:33:38 +0000 (14:33 -0400)
Change-Id: I370944339d0240d5dea4d124a9c9c03ae246020d
Signed-off-by: Tommy Carpenter <tommy@research.att.com>
31 files changed:
.gitignore [new file with mode: 0644]
.gitreview [new file with mode: 0644]
Dockerfile [new file with mode: 0644]
LICENSE.txt [new file with mode: 0644]
a1/__init__.py [new file with mode: 0644]
a1/a1rmr.py [new file with mode: 0644]
a1/controller.py [new file with mode: 0644]
a1/exceptions.py [new file with mode: 0644]
a1/openapi.yaml [new file with mode: 0644]
a1/run.py [new file with mode: 0644]
a1/utils.py [new file with mode: 0644]
docs/developer-guide.rst [new file with mode: 0644]
docs/index.rst [new file with mode: 0644]
docs/release-notes.rst [new file with mode: 0644]
integration_tests/Dockerfile [new file with mode: 0644]
integration_tests/Dockerfile-Bombard [new file with mode: 0644]
integration_tests/bombard.py [new file with mode: 0644]
integration_tests/docker-compose.yml [new file with mode: 0644]
integration_tests/putdata [new file with mode: 0644]
integration_tests/receiver.py [new file with mode: 0644]
integration_tests/test_a1.tavern.yaml [new file with mode: 0644]
integration_tests/test_docker.rt [new file with mode: 0644]
integration_tests/test_local.rt [new file with mode: 0644]
setup.py [new file with mode: 0644]
tests/fixtures/ricmanifest.json [new file with mode: 0644]
tests/fixtures/rmr_string_int_mapping.txt [new file with mode: 0644]
tests/test_controller.py [new file with mode: 0644]
tests/test_utils.py [new file with mode: 0644]
tests/testing_helpers.py [new file with mode: 0644]
tox-integration.ini [new file with mode: 0644]
tox.ini [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..4f07413
--- /dev/null
@@ -0,0 +1,97 @@
+.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
diff --git a/.gitreview b/.gitreview
new file mode 100644 (file)
index 0000000..91493c1
--- /dev/null
@@ -0,0 +1,5 @@
+[gerrit]
+host=gerrit.o-ran-sc.org
+port=29418
+project=ric-plt/a1/
+defaultbranch=master
diff --git a/Dockerfile b/Dockerfile
new file mode 100644 (file)
index 0000000..8b44500
--- /dev/null
@@ -0,0 +1,41 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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.7
+
+ADD . /tmp
+
+# Install RMR
+RUN apt-get update && apt-get install -y gcc git cmake
+RUN git clone https://gerrit.oran-osc.org/r/ric-plt/lib/rmr
+WORKDIR rmr
+RUN git checkout a012cf63dfdad3656c995cb06c316fd208c63b98
+RUN mkdir .build; cd .build; cmake ..; make install
+
+# Install python-rmr
+RUN pip install --upgrade pip 
+
+#install a1
+WORKDIR /tmp
+RUN pip install .
+EXPOSE 10000
+
+# rmr setups
+RUN mkdir -p /opt/route/
+ENV LD_LIBRARY_PATH /usr/local/lib
+ENV RMR_SEED_RT /opt/route/local.rt
+
+CMD run.py
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644 (file)
index 0000000..f836918
--- /dev/null
@@ -0,0 +1,28 @@
+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/a1/__init__.py b/a1/__init__.py
new file mode 100644 (file)
index 0000000..6212289
--- /dev/null
@@ -0,0 +1,36 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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 logging
+import connexion
+
+
+def get_module_logger(mod_name):
+    """
+    To use this, do logger = get_module_logger(__name__)
+    """
+    logger = logging.getLogger(mod_name)
+    handler = logging.StreamHandler()
+    formatter = logging.Formatter(
+        '%(asctime)s [%(name)-12s] %(levelname)-8s %(message)s')
+    handler.setFormatter(formatter)
+    logger.addHandler(handler)
+    logger.setLevel(logging.DEBUG)
+    return logger
+
+
+app = connexion.App(__name__, specification_dir='.')
+app.add_api('openapi.yaml', arguments={'title': 'My Title'})
diff --git a/a1/a1rmr.py b/a1/a1rmr.py
new file mode 100644 (file)
index 0000000..589affd
--- /dev/null
@@ -0,0 +1,163 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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 os
+import gevent
+from rmr import rmr
+from a1 import get_module_logger
+from a1.exceptions import MessageSendFailure, ExpectedAckNotReceived
+
+logger = get_module_logger(__name__)
+
+
+RMR_RCV_RETRY_INTERVAL = int(os.environ.get("RMR_RCV_RETRY_INTERVAL", 1000))
+RETRY_TIMES = int(os.environ.get("RMR_RETRY_TIMES", 4))
+MRC = None
+
+
+RECEIVED_MESSAGES = []  # used to store messages we need but havent been procedded yet
+WAITING_TRANSIDS = {}  # used to store transactionids we are waiting for, so we can filter other stuff out
+
+
+def _dequeue_all_waiting_messages():
+    """
+    dequeue all waiting rmr messages from rmr, put them into RECEIVED_MESSAGES
+    """
+    new_messages = []
+    sbuf = rmr.rmr_alloc_msg(MRC, 4096)
+    while True:
+        sbuf = rmr.rmr_torcv_msg(MRC, sbuf, 0)  # set the timeout to 0 so this doesn't block!!
+        summary = rmr.message_summary(sbuf)
+        if summary["message state"] == 12 and summary["message status"] == "RMR_ERR_TIMEOUT":
+            break
+        elif summary["transaction id"] in WAITING_TRANSIDS:  # message is relevent
+            new_messages.append(summary)
+        else:
+            logger.debug("A message was received by a1, but a1 was not expecting it! It's being dropped: %s", summary)
+            # do nothing with message, effectively dropped
+    return new_messages
+
+
+def _check_if_ack_received(target_transid, target_type):
+    """
+    Try to recieve the latest messages, then search the current queue for the target ACK
+    TODO: probably a slightly more efficient data structure than list. Maybe a dict by message type
+        However, in the near term, where there are not many xapps under A1, this is fine. Revisit later.
+    TODO: do we need to deal with duplicate ACKs for the same transaction id?
+        Is it possible if the downstream xapp uses rmr_rts? Might be harmless to sit in queue.. might slow things
+
+    """
+    new_messages = _dequeue_all_waiting_messages()  # dequeue all waiting messages
+    global RECEIVED_MESSAGES  # this is ugly, but fine.. we just need an in memory list across the async calls
+    RECEIVED_MESSAGES += new_messages
+    for index, summary in enumerate(RECEIVED_MESSAGES):  # Search the queue for the target message
+        if (
+            summary["message state"] == 0
+            and summary["message status"] == "RMR_OK"
+            and summary["message type"] == target_type
+            and summary["transaction id"] == target_transid
+        ):  # Found; delete it from queue
+            del RECEIVED_MESSAGES[index]
+            return summary
+    return None
+
+
+def init_rmr():
+    """
+    called from run; not called for unit tests
+    """
+    global MRC
+    MRC = rmr.rmr_init(b"4562", rmr.RMR_MAX_RCV_BYTES, 0x00)
+
+    while rmr.rmr_ready(MRC) == 0:
+        gevent.sleep(1)
+        logger.debug("not yet ready")
+
+
+def send(payload, message_type=0):
+    """
+    sends a message up to RETRY_TIMES
+    If the message is sent successfully, it returns the transactionid
+    Raises an exception (MessageSendFailure) otherwise
+    """
+    # we may be called many times in asyncronous loops, so for now, it is safer not to share buffers. We can investifgate later whether this is really a problem.
+    sbuf = rmr.rmr_alloc_msg(MRC, 4096)
+    payload = payload if isinstance(payload, bytes) else payload.encode("utf-8")
+
+    # retry RETRY_TIMES to send the message
+    tried = 0
+    while True:
+        # setup the send message
+        rmr.set_payload_and_length(payload, sbuf)
+        rmr.generate_and_set_transaction_id(sbuf)
+        sbuf.contents.state = 0
+        sbuf.contents.mtype = message_type
+        pre_send_summary = rmr.message_summary(sbuf)
+        logger.debug("Pre message send summary: %s", pre_send_summary)
+        transaction_id = pre_send_summary["transaction id"]  # save the transactionid because we need it later
+
+        # send
+        sbuf = rmr.rmr_send_msg(MRC, sbuf)
+        post_send_summary = rmr.message_summary(sbuf)
+        logger.debug("Post message send summary: %s", rmr.message_summary(sbuf))
+
+        # check success or failure
+        if post_send_summary["message state"] == 0 and post_send_summary["message status"] == "RMR_OK":
+            return transaction_id  # we are good
+        if post_send_summary["message state"] == 10 and post_send_summary["message status"] == "RMR_ERR_RETRY":
+            # in this state, we should retry
+            if tried == RETRY_TIMES:
+                # we have tried RETRY_TIMES and we are still not getting a good state, raise an exception and let the caller deal with it
+                raise MessageSendFailure(str(post_send_summary))
+            else:
+                tried += 1
+        else:
+            # we hit a state where we should not even retry
+            raise MessageSendFailure(str(post_send_summary))
+
+
+def send_ack_retry(payload, expected_ack_message_type, message_type=0):
+    """
+    send a message and check for an ACK.
+    If no ACK is recieved, defer execution for RMR_RCV_RETRY_INTERVAL ms, then check again.
+    If no ack is received before the timeout (set by _rmr_init), send again and try again up to RETRY_TIMES
+
+    It is critical here to set the RMR_TIMEOUT to 0 in the rmr_rcv_to function, which causes that function NOT to block.
+    Instead, if the message isn't there, we give up execution for the interval, which allows the gevent server to process other requests in the meantime.
+
+    Amazing props to https://sdiehl.github.io/gevent-tutorial/
+    (which also runs this whole server)
+    """
+
+    # try to send the msg to the downstream policy handler
+    expected_transaction_id = send(payload, message_type)
+    WAITING_TRANSIDS[expected_transaction_id] = 1
+
+    gevent.sleep(0.01)  # wait 10ms before we try the first recieve
+    for _ in range(0, RETRY_TIMES):
+        logger.debug("Seeing if return message is fufilled")
+        summary = _check_if_ack_received(expected_transaction_id, expected_ack_message_type)
+        if summary:
+            logger.debug("Target ack Message received!: %s", summary)
+            logger.debug("current queue size is %d", len(RECEIVED_MESSAGES))
+            del WAITING_TRANSIDS[expected_transaction_id]
+            return summary["payload"]
+        else:
+            logger.debug("Deffering execution for %s seconds", str(RMR_RCV_RETRY_INTERVAL / 1000))
+            gevent.sleep(RMR_RCV_RETRY_INTERVAL / 1000)
+
+    # we still didn't get the ACK we want
+    raise ExpectedAckNotReceived()
diff --git a/a1/controller.py b/a1/controller.py
new file mode 100644 (file)
index 0000000..ca9618b
--- /dev/null
@@ -0,0 +1,113 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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 flask import Response
+import connexion
+import json
+from a1 import get_module_logger
+from a1 import a1rmr, exceptions, utils
+
+
+logger = get_module_logger(__name__)
+
+
+def _get_needed_policy_info(policyname):
+    """
+    Get the needed info for a policy
+    """
+    # Currently we read the manifest on each call, which would seem to allow updating A1 in place. Revisit this?
+    manifest = utils.get_ric_manifest()
+    for m in manifest["controls"]:
+        if m["name"] == policyname:
+            schema = m["message_receives_payload_schema"] if "message_receives_payload_schema" in m else None
+            return (
+                utils.rmr_string_to_int(m["message_receives_rmr_type"]),
+                schema,
+                utils.rmr_string_to_int(m["message_sends_rmr_type"]),
+            )
+    raise exceptions.PolicyNotFound()
+
+
+def _try_func_return(func):
+    """
+    generic caller that returns the apporp http response if exceptions are raised
+    """
+    try:
+        return func()
+    except exceptions.PolicyNotFound as exc:
+        logger.exception(exc)
+        return "", 404
+    except exceptions.MissingManifest as exc:
+        logger.exception(exc)
+        return "A1 was unable to find the required RIC manifest. report this!", 500
+    except exceptions.MissingRmrString as exc:
+        logger.exception(exc)
+        return "A1 does not have a mapping for the desired rmr string. report this!", 500
+    except exceptions.MessageSendFailure as exc:
+        logger.exception(exc)
+        return "A1 was unable to send a needed message to a downstream subscriber", 504
+    except exceptions.ExpectedAckNotReceived as exc:
+        logger.exception(exc)
+        return "A1 was expecting an ACK back but it didn't receive one or didn't recieve the expected ACK", 504
+    except BaseException as exc:
+        # catch all, should never happen...
+        logger.exception(exc)
+        return Response(status=500)
+
+
+def _put_handler(policyname, data):
+    """
+    Handles policy put
+    """
+
+    mtype_send, schema, mtype_return = _get_needed_policy_info(policyname)
+
+    # validate the PUT against the schema, or if there is no shema, make sure the pUT is empty
+    if schema:
+        utils.validate_json(data, schema)
+    elif data != {}:
+        return "BODY SUPPLIED BUT POLICY HAS NO EXPECTED BODY", 400
+
+    # send rmr, wait for ACK
+    return_payload = a1rmr.send_ack_retry(json.dumps(data), message_type=mtype_send, expected_ack_message_type=mtype_return)
+
+    # right now it is assumed that xapps respond with JSON payloads
+    # it is further assumed that they include a field "status" and that the value "SUCCESS" indicates a good policy change
+    try:
+        rpj = json.loads(return_payload)
+        return (rpj, 200) if rpj["status"] == "SUCCESS" else ({"reason": "BAD STATUS", "return_payload": rpj}, 502)
+    except json.decoder.JSONDecodeError:
+        return {"reason": "NOT JSON", "return_payload": return_payload}, 502
+    except KeyError:
+        return {"reason": "NO STATUS", "return_payload": rpj}, 502
+
+
+# Public
+
+
+def put_handler(policyname):
+    """
+    Handles policy replacement
+    """
+    data = connexion.request.json
+    return _try_func_return(lambda: _put_handler(policyname, data))
+
+
+def get_handler(policyname):
+    """
+    Handles policy GET
+    """
+    return "", 501
diff --git a/a1/exceptions.py b/a1/exceptions.py
new file mode 100644 (file)
index 0000000..055cafb
--- /dev/null
@@ -0,0 +1,43 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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.
+# ==================================================================================
+"""
+Custom Exceptions
+"""
+
+
+class MessageSendFailure(BaseException):
+    pass
+
+
+class ExpectedAckNotReceived(BaseException):
+    pass
+
+
+class PolicyNotFound(BaseException):
+    pass
+
+
+class MissingRmrString(BaseException):
+    pass
+
+
+class MissingManifest(BaseException):
+    pass
+
+
+class MissingRmrMapping(BaseException):
+    pass
diff --git a/a1/openapi.yaml b/a1/openapi.yaml
new file mode 100644 (file)
index 0000000..aa02cc6
--- /dev/null
@@ -0,0 +1,77 @@
+openapi: 3.0.0
+info:
+  version: 0.8.0
+  title: RIC A1
+paths:
+  '/ric/policies/{policyname}':
+    parameters:
+      - name: policyname
+        in: path
+        description: the name of the policy to retrieve or replace
+        required: true
+        schema:
+          type: string
+    put:
+      description: >
+          Replace the current operation of policyname with the new parameters (replaces the current policy with the new one specified here).
+
+
+          Until there are standard policy definitions that are defined OUTSIDE of the scope of xapps, this API will be *very underspecified*.
+          This is a known gap, do not despair.
+          The PUT body is specified, *currently* in the xapp manifest that implements this policy; the caller should refer to the message_receives_payload_schema field to make this request.
+          The return content is also specified as above (in the xapp manifest) in the message_sends_payload_schema field.
+
+
+          Eventually, we need concrete policy defintions that are decoupled from xapp, and then this API description will become more fully specified.
+      tags:
+        - A1 Mediator
+      operationId: a1.controller.put_handler
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+
+
+      responses:
+        '200':
+          description: >
+            The downstream component responsible for implementing this policy replied with a good response. Check the manifest for response details.
+        '400':
+          description: >
+            Bad PUT body for this policyname
+        '404':
+          description: >
+            there is no policy with this name
+        '502':
+          description: >
+            The xapp that implements this policy replied, but the reply was a "failure", OR there was no status indicating success or failure.
+            This returns an object containing the reason, and the return payload.
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  reason:
+                    type: string
+                    enum: [
+                        "NO STATUS",
+                        "BAD STATUS",
+                        "NOT JSON"
+                    ]
+                  return_payload:
+                    type: object
+
+        '504':
+          description: >
+            the downstream component responsible for handling this policy did not respond (in time)
+
+    get:
+      description: Get the current state/value of policyname
+      tags:
+        - A1 Mediator
+      operationId: a1.controller.get_handler
+      responses:
+        '501':
+          description: >
+            "future GET support has been pondered, but this is not currently implemented"U
diff --git a/a1/run.py b/a1/run.py
new file mode 100644 (file)
index 0000000..903fe85
--- /dev/null
+++ b/a1/run.py
@@ -0,0 +1,40 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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 gevent.pywsgi import WSGIServer
+from a1 import get_module_logger, app
+from a1 import utils, exceptions
+from a1.a1rmr import init_rmr
+import sys
+
+
+logger = get_module_logger(__name__)
+
+
+def main():
+    """Entrypoint"""
+    # Fail fast if we don't have a manifest
+    try:
+        utils.get_ric_manifest()
+    except exceptions.MissingManifest:
+        logger.error("Failing fast: no A1 manifest found!")
+        sys.exit(1)
+
+    logger.debug("Initializing rmr")
+    init_rmr()
+    logger.debug("Starting gevent server")
+    http_server = WSGIServer(('', 10000), app)
+    http_server.serve_forever()
diff --git a/a1/utils.py b/a1/utils.py
new file mode 100644 (file)
index 0000000..21d6d57
--- /dev/null
@@ -0,0 +1,75 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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
+from jsonschema import validate
+from a1 import get_module_logger
+from a1 import exceptions
+
+
+logger = get_module_logger(__name__)
+
+
+def _get_rmr_mapping_table(cache={}):
+    """
+    Get the rmr mapping file
+    Broken out for ease of unit testing
+    """
+    try:
+        return open("/opt/rmr_string_int_mapping.txt", "r").readlines()
+    except FileNotFoundError:
+        logger.error("Missing RMR Mapping!")
+        raise exceptions.MissingRmrMapping
+
+
+# Public
+
+
+def validate_json(instance, schema):
+    """
+    validate incoming policy payloads
+    """
+    validate(instance=instance, schema=schema)
+
+
+def get_ric_manifest():
+    """
+    Get the ric level manifest
+    """
+    try:
+        with open("/opt/ricmanifest.json", "r") as f:
+            content = f.read()
+            manifest = json.loads(content)
+            return manifest
+    except FileNotFoundError:
+        logger.error("Missing A1 Manifest!")
+        raise exceptions.MissingManifest
+
+
+def rmr_string_to_int(rmrs, cache={}):
+    """
+    map an rmr string to an int.
+    TODO: should we allow for dynamic updates to this file? If so, we shouldn't cache
+    """
+    if cache == {}:  # fill the cache if not populated
+        lines = _get_rmr_mapping_table()
+        for l in lines:
+            items = l.split(":")
+            cache[items[0]] = int(items[1])
+
+    if rmrs in cache:
+        return cache[rmrs]
+    raise exceptions.MissingRmrString(rmrs)
diff --git a/docs/developer-guide.rst b/docs/developer-guide.rst
new file mode 100644 (file)
index 0000000..57c7002
--- /dev/null
@@ -0,0 +1,90 @@
+A1
+==
+
+Tech Stack
+==========
+
+-  OpenAPI3
+-  Connexion
+-  Flask with Gevent serving
+-  Python3.7
+
+Version bumping
+===============
+
+This project follows semver. When changes are made, the versions are in:
+
+1) ``docs/release-notes.rst``
+
+2) ``setup.py``
+
+3) ``a1/openapi.yml``
+
+Running locally
+===============
+
+1. This requires that RMR is installed on the base system. (the
+   Dockerfile does this when running in Docker)
+
+2. It also requires rmr-python >= 0.10.1 installed. (The dockerfile also
+   does this)
+
+3. Create a ``local.rt`` file and copy it into ``/opt/route/local.rt``.
+   Note, the example one in ``local_tests`` will need to be modified for
+   your scenario and machine.
+
+4. Copy a ric manifest into ``/opt/ricmanifest.json`` and an rmr mapping
+   table into ``/opt/rmr_string_int_mapping.txt``. You can use the test
+   ones packaged if you want:
+
+   ::
+
+     cp tests/fixtures/ricmanifest.json /opt/ricmanifest.json cp
+     tests/fixtures/rmr_string_int_mapping.txt
+     /opt/rmr_string_int_mapping.txt
+
+5. Then:
+
+   sudo pip install –ignore-installed .; set -x LD_LIBRARY_PATH
+   /usr/local/lib/; set -x RMR_SEED_RT /opt/route/local.rt ; set -x
+   RMR_RCV_RETRY_INTERVAL 500; set -x RMR_RETRY_TIMES 10;
+   /usr/bin/run.py
+
+Testing locally
+===============
+
+There are also two test receivers in ``localtests`` you can run locally.
+The first is meant to be used with the ``control_admission`` policy
+(that comes in test fixture ric manifest):
+
+::
+
+   set -x LD_LIBRARY_PATH /usr/local/lib/; set -x RMR_SEED_RT /opt/route/local.rt ; python receiver.py
+
+The second can be used against the ``test_policy`` policy to test the
+async nature of A1, and to test race conditions. You can start it with
+several env variables as follows:
+
+::
+
+   set -x LD_LIBRARY_PATH /usr/local/lib/; set -x RMR_SEED_RT /opt/route/local.rt ; set -x TEST_RCV_PORT 4563; set -x TEST_RCV_RETURN_MINT 10001; set -x TEST_RCV_SEC_DELAY 5; set -x TEST_RCV_RETURN_PAYLOAD '{"ACK_FROM": "DELAYED_TEST", "status": "SUCCESS"}' ; python receiver.py
+
+To test the async nature of A1, trigger a call to ``test_policy``, which
+will target the delayed receicer, then immediately call
+``control_admission``. The ``control_admission`` policy return should be
+returned immediately, whereas the ``test_policy`` should return after
+about ``TEST_RCV_SEC_DELAY 5``. The ``test_policy`` should not block A1
+while it is sleeping, and both responses should be correct.
+
+::
+
+   curl -v -X PUT -H "Content-Type: application/json" -d '{}' localhost:10000/ric/policies/test_policy
+   curl -v -X PUT -H "Content-Type: application/json" -d '{"dc_admission_start_time": "10:00:00", "dc_admission_end_time": "11:00:00"}' localhost:10000/ric/policies/control_admission_time
+
+Finally, there is a test “bombarder” that will flood A1 with messages
+with good message types but bad transaction IDs, to test A1’s resilience
+against queue-overflow attacks
+
+::
+
+   set -x LD_LIBRARY_PATH /usr/local/lib/; set -x RMR_SEED_RT /opt/route/local.rt ;  python bombard.py
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644 (file)
index 0000000..0172cff
--- /dev/null
@@ -0,0 +1,69 @@
+A1
+==
+
+API
+===
+
+You can see the API (OpenAPI3 spec) at ``a1/openapi.yml``. You can also
+see the “pretty” version if you run the container at
+``http://localhost:10000/ui/``.
+
+Unit Testing
+============
+
+Note, this requires rmr to be installed!
+
+::
+
+   tox
+   open htmlcov/index.html
+
+Integration testing
+===================
+
+This tests A1’s external API with two test receivers. Note, this
+currently depends on docker-compose, meaning you cannot run this if
+docker-compose is not installed. Note2: this is not fast. It builds the
+containers and launches requests against the API so it takes time.
+
+::
+
+   tox -c tox-integration.ini
+
+Running
+=======
+
+Optional ENV Variables
+----------------------
+
+You can set the following ENVs to change the A1 behavior: 1)
+``RMR_RCV_RETRY_INTERVAL`` the number of milliseconds that execution
+will defer (back to the server loop to handle http request if
+applicable) when an expected ack is not received by rmr call. The
+default is ``1000`` (1s). The time for the full HTTP request to
+``PUT /policies`` will be > this if an ACK is not recieved within 10ms,
+which is an initial delay until the first rcv is tried. 2)
+``RMR_RETRY_TIMES`` the number of times failed rmr operations such as
+timeouts and send failures should be retried before A1 gives up and
+returns a 503. The default is ``4``.
+
+Docker
+------
+
+building
+~~~~~~~~
+
+::
+
+   docker build --no-cache -t a1:X.Y.Z .
+
+.. _running-1:
+
+running
+~~~~~~~
+
+(TODO: this will be enhanced with Helm.)
+
+::
+
+   docker run -dt -p 10000:10000 -v /path/to/localrt:/opt/route/local.rt -v /path/to/ricmanifest:/opt/ricmanifest.json a1:X.Y.Z -v
diff --git a/docs/release-notes.rst b/docs/release-notes.rst
new file mode 100644 (file)
index 0000000..aa444ca
--- /dev/null
@@ -0,0 +1,169 @@
+Change Log
+==========
+
+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.8.0] - 5/28/2019
+-------------------
+
+::
+
+   * Convert docs to appropriate format
+   * Move rmr string to int mapping to a file
+
+
+
+[0.7.2] - 5/24/2019
+-------------------
+
+::
+
+   * Use tavern to test the actual running docker container
+   * Restructures the integration tests to run as a single tox command
+   * Re-ogranizes the README and splits out the Developers guide, which is not needed by users.
+
+.. _section-1:
+
+[0.7.1] - 5/23/2019
+-------------------
+
+::
+
+   * Adds a defense mechanism against A1 getting queue-overflowed with messages A1 doesnt care about; A1 now ignores all incoming messages it's not waiting for, so it's queue size should now always be "tiny", i.e., never exceeding the number of valid requests it's waiting for ACKs back for
+   * Adds a test "bombarding" script that tests this
+
+.. _section-2:
+
+[0.7.0] - 5/22/19
+-----------------
+
+::
+
+   * Main purpose of this change is to fix a potential race condition where A1 sends out M1 expecting ACK1, and while waiting for ACK1, sends out M2 expecting ACK2, but gets back ACK2, ACK1. Prior to this change, A1 may have eaten ACK2 and never fufilled the ACK1 request.
+   * Fix a bug in the unit tests (found using a fresh container with no RIC manifest!)
+   * Fix a (critical) bug in a1rmr due to a rename in the last iteration (RMR_ERR_RMR_RCV_RETRY_INTERVAL)
+   * Make unit tests faster by setting envs in tox
+   * Move to the now publically available rmr-python
+   * Return a 400 if am xapp does not expect a body, but the PUT provides one
+   * Adds a new test policy to the example RIC manifest and a new delayed receiver to test the aformentiond race condition
+
+.. _section-3:
+
+[0.6.0]
+-------
+
+::
+
+   * Upgrade to rmr 0.10.0
+   * Fix bad api spec RE GET
+   * Fix a (big) bug where transactionid wasn't being checked, which wouldn't have worked on sending two policies to the same downstream policy handler
+
+.. _section-4:
+
+[0.5.1] - 5/13/2019
+-------------------
+
+::
+
+   * Rip some testing structures out of here that should have been in rmr (those are now in rmr 0.9.0, upgrade to that)
+   * Run Python BLACK for formatting
+
+.. _section-5:
+
+[0.5.0] - 5/10/2019
+-------------------
+
+::
+
+   * Fix a blocking execution bug by moving from rmr's timeout to a non blocking call + retry loop + asyncronous sleep
+   * Changes the ENV RMR_RCV_TIMEOUT to RMR_RCV_RETRY_INTERVAL
+
+.. _section-6:
+
+[0.4.0] - 5/9.2019
+------------------
+
+::
+
+   * Update to rmr 0.8.3
+   * Change 503 to 504 for the case where downstream does not reply, per recommendation
+   * Add a 502 with different reasons if the xapp replies but with a bad/malformed/missing status
+   * Make testing much more modular, in anticipating of moving some unit test functionality into rmr itself
+
+.. _section-7:
+
+[0.3.4] - 5/8/2019
+------------------
+
+::
+
+   * Crash immediately if manifest isn't mounted
+   * Add unit tests for utils
+   * Add missing lic
+
+.. _section-8:
+
+[0.3.3]
+-------
+
+::
+
+   * Upgrade A1 to rmr 0.8.0
+   * Go from deb RMR installation to git
+   * Remove obnoxious receiver logging
+
+.. _section-9:
+
+[0.3.2]
+-------
+
+::
+
+   * Upgrade A1 to rmr 0.6.0
+
+.. _section-10:
+
+[0.3.1]
+-------
+
+::
+
+   * Add license headers
+
+.. _section-11:
+
+[0.3.0]
+-------
+
+::
+
+   * Introduce RIC Manifest
+   * Move some testing functionality into a helper module
+   * Read the policyname to rmr type mapping from manifest
+   * Do PUT payload validation based on the manifest
+
+.. _section-12:
+
+[0.2.0]
+-------
+
+::
+
+   * Bump rmr python dep version
+   * Include a Dockerized test receiver
+   * Stencil out the mising GET
+   * Update the OpenAPI
+   * Include a test docker compose file
+
+.. _section-13:
+
+[0.1.0]
+-------
+
+::
+
+   * Initial Implementation
diff --git a/integration_tests/Dockerfile b/integration_tests/Dockerfile
new file mode 100644 (file)
index 0000000..fb37bbb
--- /dev/null
@@ -0,0 +1,38 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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.7
+
+ADD receiver.py /
+
+# Install RMR
+RUN apt-get update && apt-get install -y gcc git cmake
+RUN git clone https://gerrit.oran-osc.org/r/ric-plt/lib/rmr
+WORKDIR rmr
+RUN git checkout a012cf63dfdad3656c995cb06c316fd208c63b98
+RUN mkdir .build; cd .build; cmake ..; make install
+
+# Install python-rmr
+RUN pip install --upgrade pip 
+RUN pip install rmr==0.10.1
+
+# rmr setups
+RUN mkdir -p /opt/route/
+ENV LD_LIBRARY_PATH /usr/local/lib
+ENV RMR_SEED_RT /opt/route/local.rt
+
+WORKDIR /
+CMD ["python","-u","receiver.py"]
diff --git a/integration_tests/Dockerfile-Bombard b/integration_tests/Dockerfile-Bombard
new file mode 100644 (file)
index 0000000..83ad4bd
--- /dev/null
@@ -0,0 +1,38 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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.7
+
+ADD bombard.py /
+
+# Install RMR
+RUN apt-get update && apt-get install -y gcc git cmake
+RUN git clone https://gerrit.oran-osc.org/r/ric-plt/lib/rmr
+WORKDIR rmr
+RUN git checkout a012cf63dfdad3656c995cb06c316fd208c63b98
+RUN mkdir .build; cd .build; cmake ..; make install
+
+# Install python-rmr
+RUN pip install --upgrade pip 
+RUN pip install rmr==0.10.1
+
+# rmr setups
+RUN mkdir -p /opt/route/
+ENV LD_LIBRARY_PATH /usr/local/lib
+ENV RMR_SEED_RT /opt/route/local.rt
+
+WORKDIR /
+CMD ["python","-u","bombard.py"]
diff --git a/integration_tests/bombard.py b/integration_tests/bombard.py
new file mode 100644 (file)
index 0000000..f0a501b
--- /dev/null
@@ -0,0 +1,47 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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 time
+import random
+import string
+import os
+import signal
+import sys
+from rmr import rmr
+
+
+DELAY_MS = int(os.environ.get("BOMBARD_DELAY_MS", 100))
+
+# Init rmr
+mrc = rmr.rmr_init(b"4565", rmr.RMR_MAX_RCV_BYTES, 0x00)
+while rmr.rmr_ready(mrc) == 0:
+    time.sleep(1)
+    print("not yet ready")
+rmr.rmr_set_stimeout(mrc, 2)
+sbuf = rmr.rmr_alloc_msg(mrc, 256)
+
+
+while True:
+    # generate a random value between 1 and 256 bytes, then gen some random  bytes with several nulls thrown in
+    val = "BOMBS AWAY".encode("utf8")
+    rmr.set_payload_and_length(val, sbuf)
+    rmr.generate_and_set_transaction_id(sbuf)
+    sbuf.contents.state = 0
+    sbuf.contents.mtype = random.choice([20001, 10001])
+    print("Pre send summary: {}".format(rmr.message_summary(sbuf)))
+    sbuf = rmr.rmr_send_msg(mrc, sbuf)
+    print("Post send summary: {}".format(rmr.message_summary(sbuf)))
+    time.sleep(0.001 * DELAY_MS)
diff --git a/integration_tests/docker-compose.yml b/integration_tests/docker-compose.yml
new file mode 100644 (file)
index 0000000..0deec85
--- /dev/null
@@ -0,0 +1,63 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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.
+# ==================================================================================
+version: '3'
+services:
+
+    rmr_receiver:
+        build: .
+        hostname: "rmr_receiver"
+        volumes:
+          - ./test_docker.rt:/opt/route/local.rt
+
+    rmr_delay_receiver:
+        build: .
+        hostname: "rmr_delay_receiver"
+        volumes:
+          - ./test_docker.rt:/opt/route/local.rt
+        environment:
+            # https://github.com/docker/compose/issues/3878
+            RMR_RCV_RETRY_INTERVAL: 500
+            RMR_RETRY_TIMES: 10
+            TEST_RCV_PORT: 4563
+            TEST_RCV_RETURN_MINT: 10001
+            TEST_RCV_SEC_DELAY: 5
+            TEST_RCV_RETURN_PAYLOAD: '{"ACK_FROM": "DELAYED_TEST", "status": "SUCCESS"}'
+
+#    bombarder:
+#        build:
+#            context: .
+#            dockerfile: Dockerfile-Bombard
+#        hostname: "bombarder"
+#        volumes:
+#          - /tmp/local.rt:/opt/route/local.rt
+#        environment:
+#            BOMBARD_DELAY_MS: 100
+#
+    a1:
+        image: a1:latest
+        build:
+            context: ..
+        hostname: "a1"
+        volumes:
+          - ./test_docker.rt:/opt/route/local.rt
+          - ./../tests/fixtures/ricmanifest.json:/opt/ricmanifest.json
+          - ./../tests/fixtures/rmr_string_int_mapping.txt:/opt/rmr_string_int_mapping.txt
+        ports:
+            - "10000:10000"
+        environment:
+            RMR_RCV_RETRY_INTERVAL: 500
+            RMR_RETRY_TIMES: 20
diff --git a/integration_tests/putdata b/integration_tests/putdata
new file mode 100644 (file)
index 0000000..329851c
--- /dev/null
@@ -0,0 +1 @@
+{"dc_admission_start_time": "10:00:00", "dc_admission_end_time": "11:00:00"}
diff --git a/integration_tests/receiver.py b/integration_tests/receiver.py
new file mode 100644 (file)
index 0000000..3a48a8d
--- /dev/null
@@ -0,0 +1,57 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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.
+# ==================================================================================
+"""
+Test receiver
+"""
+
+import time
+from rmr import rmr
+import json
+import os
+
+PORT = os.environ.get("TEST_RCV_PORT", "4560")
+RETURN_MINT = int(os.environ.get("TEST_RCV_RETURN_MINT", 20001))
+DELAY = int(os.environ.get("TEST_RCV_SEC_DELAY", 0))
+PAYLOAD_RETURNED = json.loads(
+    os.environ.get("TEST_RCV_RETURN_PAYLOAD", '{"ACK_FROM": "ADMISSION_CONTROL", "status": "SUCCESS"}')
+)
+
+# TODO: should these be made constants?
+mrc = rmr.rmr_init(PORT.encode("utf-8"), rmr.RMR_MAX_RCV_BYTES, 0x00)
+
+while rmr.rmr_ready(mrc) == 0:
+    time.sleep(1)
+    print("not yet ready")
+
+print("listening")
+sbuf = None
+while True:
+    sbuf = rmr.rmr_torcv_msg(mrc, sbuf, 1000)
+    summary = rmr.message_summary(sbuf)
+    if summary["message state"] == 12 and summary["message status"] == "RMR_ERR_TIMEOUT":
+        # print("Nothing received yet")
+        time.sleep(1)
+    else:
+        print("Message received!: {}".format(summary))
+
+        val = json.dumps(PAYLOAD_RETURNED).encode("utf-8")
+        rmr.set_payload_and_length(val, sbuf)
+        sbuf.contents.mtype = RETURN_MINT
+        print("Pre reply summary: {}".format(rmr.message_summary(sbuf)))
+        time.sleep(DELAY)
+        sbuf = rmr.rmr_rts_msg(mrc, sbuf)
+        print("Post reply summary: {}".format(rmr.message_summary(sbuf)))
diff --git a/integration_tests/test_a1.tavern.yaml b/integration_tests/test_a1.tavern.yaml
new file mode 100644 (file)
index 0000000..5ecb69a
--- /dev/null
@@ -0,0 +1,74 @@
+# test_a1.tavern.yaml
+
+---
+
+test_name: test delayed policy
+
+stages:
+  - name: test the delayed policy
+    request:
+      url: http://localhost:10000/ric/policies/test_policy
+      method: PUT
+      json:
+        {}
+      headers:
+        content-type: application/json
+    response:
+      status_code: 200
+      body:
+        ACK_FROM: DELAYED_TEST
+        status: SUCCESS
+
+
+---
+
+test_name: test admission control
+
+stages:
+  - name: test the admission control policy
+    request:
+      url: http://localhost:10000/ric/policies/control_admission_time
+      method: PUT
+      json:
+        dc_admission_start_time: "10:00:00"
+        dc_admission_end_time: "11:00:00"
+      headers:
+        content-type: application/json
+    response:
+      status_code: 200
+      body:
+        ACK_FROM: ADMISSION_CONTROL
+        status: SUCCESS
+
+---
+
+test_name: bad_requests
+
+stages:
+  - name: does not exist
+    request:
+      url: http://localhost:10000/ric/policies/darkness
+      method: PUT
+      json:
+        {}
+      headers:
+        content-type: application/json
+    response:
+      status_code: 404
+
+  - name: not a json
+    request:
+      url: http://localhost:10000/ric/policies/control_admission_time
+      method: PUT
+      data: "asdf"
+    response:
+      status_code: 415
+
+  - name: body not expected
+    request:
+      url: http://localhost:10000/ric/policies/test_policy
+      method: PUT
+      json:
+        not: "welcome"
+    response:
+      status_code: 400
diff --git a/integration_tests/test_docker.rt b/integration_tests/test_docker.rt
new file mode 100644 (file)
index 0000000..2173707
--- /dev/null
@@ -0,0 +1,6 @@
+newrt|start
+rte|10000|rmr_delay_receiver:4563
+rte|20000|rmr_receiver:4560
+rte|10001|a1:4562
+rte|20001|a1:4562
+newrt|end
diff --git a/integration_tests/test_local.rt b/integration_tests/test_local.rt
new file mode 100644 (file)
index 0000000..92f9ca4
--- /dev/null
@@ -0,0 +1,6 @@
+newrt|start
+rte|10000|devarchwork:4563
+rte|20000|devarchwork:4560
+rte|10001|devarchwork:4562
+rte|20001|devarchwork:4562
+newrt|end
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..6050ba9
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,30 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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="a1",
+    version="0.8.0",
+    packages=find_packages(exclude=["tests.*", "tests"]),
+    author="Tommy Carpenter",
+    description="RIC A1 Mediator for policy/intent changes",
+    url="",
+    entry_points={"console_scripts": ["run.py=a1.run:main"]},
+    # we require jsonschema, should be in that list, but connexion already requires a specific version of it
+    install_requires=["requests", "Flask", "connexion[swagger-ui]", "gevent", "rmr>=0.10.0"],
+    package_data={"a1": ["openapi.yaml"]},
+)
diff --git a/tests/fixtures/ricmanifest.json b/tests/fixtures/ricmanifest.json
new file mode 100644 (file)
index 0000000..211a115
--- /dev/null
@@ -0,0 +1,63 @@
+{
+  "controls":[
+    {
+      "name":"control_admission_time",
+      "description":"time period to allow dual connection",
+      "message_receives_rmr_type":"DC_ADMISSION_INTERVAL_CONTROL",
+      "message_receives_payload_schema":{
+        "$schema":"http://json-schema.org/draft-07/schema#",
+        "type":"object",
+        "properties":{
+          "dc_admission_start_time":{
+            "type":"string",
+            "pattern":"^[0-9]{2}:[0-9]{2}:[0-9]{2}$"
+          },
+          "dc_admission_end_time":{
+            "type":"string",
+            "pattern":"^[0-9]{2}:[0-9]{2}:[0-9]{2}$"
+          }
+        },
+        "required":[
+          "dc_admission_start_time",
+          "dc_admission_end_time"
+        ]
+      },
+      "message_sends_rmr_type":"DC_ADMISSION_INTERVAL_CONTROL_ACK",
+      "message_sends_payload_schema":{
+        "$schema":"http://json-schema.org/draft-07/schema#",
+        "type":"object",
+        "properties":{
+          "status":{
+            "type":"string",
+            "enum":[
+              "SUCCESS",
+              "FAIL"
+            ]
+          },
+          "message":{
+            "type":"string"
+          }
+        }
+      }
+    },
+    {
+      "name":"test_policy",
+      "description":"for the purposes of testing",
+      "message_receives_rmr_type":"TEST_REQ",
+      "message_sends_rmr_type":"TEST_ACK",
+      "message_sends_payload_schema":{
+        "$schema":"http://json-schema.org/draft-07/schema#",
+        "type":"object",
+        "properties":{
+          "status":{
+            "type":"string",
+            "enum":[
+              "SUCCESS",
+              "FAIL"
+            ]
+          }
+        }
+      }
+    }
+  ]
+}
diff --git a/tests/fixtures/rmr_string_int_mapping.txt b/tests/fixtures/rmr_string_int_mapping.txt
new file mode 100644 (file)
index 0000000..ff1479d
--- /dev/null
@@ -0,0 +1,4 @@
+DC_ADMISSION_INTERVAL_CONTROL:20000
+DC_ADMISSION_INTERVAL_CONTROL_ACK:20001
+TEST_REQ:10000
+TEST_ACK:10001
diff --git a/tests/test_controller.py b/tests/test_controller.py
new file mode 100644 (file)
index 0000000..680f586
--- /dev/null
@@ -0,0 +1,206 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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 tempfile
+import os
+from rmr.rmr_mocks import rmr_mocks
+from a1 import app
+from a1 import exceptions
+from rmr import rmr
+import testing_helpers
+import pytest
+
+# http://flask.pocoo.org/docs/1.0/testing/
+@pytest.fixture
+def client():
+    db_fd, app.app.config["DATABASE"] = tempfile.mkstemp()
+    app.app.config["TESTING"] = True
+    cl = app.app.test_client()
+
+    yield cl
+
+    os.close(db_fd)
+    os.unlink(app.app.config["DATABASE"])
+
+
+def _fake_dequeue(
+    monkeypatch,
+    msg_payload={"status": "SUCCESS", "foo": "bar"},
+    msg_type=20001,
+    msg_state=0,
+    jsonb=True,
+    unexpected_first=True,
+):
+    """
+    generates a mock rmr message response (returns a function that does; uses closures to set params)
+    """
+    new_messages = []
+    # stick a message we don't want at the front of the queue, then stick the message we want
+    if unexpected_first:
+        monkeypatch.setattr("rmr.rmr.rmr_torcv_msg", rmr_mocks.rcv_mock_generator(msg_payload, -1, msg_state, jsonb))
+        sbuf = rmr.rmr_alloc_msg(None, None)
+        sbuf = rmr.rmr_torcv_msg(None, sbuf, None)
+        summary = rmr.message_summary(sbuf)
+        new_messages.append(summary)
+
+    monkeypatch.setattr("rmr.rmr.rmr_torcv_msg", rmr_mocks.rcv_mock_generator(msg_payload, msg_type, msg_state, jsonb))
+    sbuf = rmr.rmr_alloc_msg(None, None)
+    sbuf = rmr.rmr_torcv_msg(None, sbuf, None)
+    summary = rmr.message_summary(sbuf)
+    new_messages.append(summary)
+
+    def f():
+        return new_messages
+
+    return f
+
+
+# Actual Tests
+
+
+def _test_put_patch(monkeypatch):
+    testing_helpers.patch_all(monkeypatch)
+    monkeypatch.setattr("rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(0))  # good sends for this whole batch
+
+    # we need to repatch alloc (already patched in patch_rmr) to fix the transactionid, alloc is called in send and recieve
+    def fake_alloc(_unused, _alsounused):
+        sbuf = rmr_mocks.Rmr_mbuf_t()
+        sbuf.contents.xaction = b"d49b53e478b711e9a1130242ac110002"
+        return sbuf
+
+    # we also need to repatch set, since in the send function, we alloc, then set a new transid
+    def fake_set_transactionid(sbuf):
+        sbuf.contents.xaction = b"d49b53e478b711e9a1130242ac110002"
+
+    # Note, we could have just patched summary, but this patches at a "lower level" so is a better test
+    monkeypatch.setattr("rmr.rmr.rmr_alloc_msg", fake_alloc)
+    monkeypatch.setattr("rmr.rmr.generate_and_set_transaction_id", fake_set_transactionid)
+
+
+def test_xapp_put_bad(client, monkeypatch):
+    """Test policy put fails"""
+    _test_put_patch(monkeypatch)
+    # return from policy handler has a status indicating FAIL
+    monkeypatch.setattr(
+        "a1.a1rmr._dequeue_all_waiting_messages", _fake_dequeue(monkeypatch, msg_payload={"status": "FAIL", "foo": "bar"})
+    )
+    res = client.put("/ric/policies/control_admission_time", json=testing_helpers.good_payload())
+    assert res.status_code == 502
+    assert res.json["reason"] == "BAD STATUS"
+    assert res.json["return_payload"] == {"status": "FAIL", "foo": "bar"}
+
+    # return from policy handler has no status field
+    monkeypatch.setattr("a1.a1rmr._dequeue_all_waiting_messages", _fake_dequeue(monkeypatch, msg_payload={"foo": "bar"}))
+    res = client.put("/ric/policies/control_admission_time", json=testing_helpers.good_payload())
+    assert res.status_code == 502
+    assert res.json["reason"] == "NO STATUS"
+    assert res.json["return_payload"] == {"foo": "bar"}
+
+    # return from policy handler not a json
+    monkeypatch.setattr(
+        "a1.a1rmr._dequeue_all_waiting_messages", _fake_dequeue(monkeypatch, msg_payload="booger", jsonb=False)
+    )
+    res = client.put("/ric/policies/control_admission_time", json=testing_helpers.good_payload())
+    assert res.status_code == 502
+    assert res.json["reason"] == "NOT JSON"
+    assert res.json["return_payload"] == "booger"
+
+    # bad type
+    monkeypatch.setattr("a1.a1rmr._dequeue_all_waiting_messages", _fake_dequeue(monkeypatch, msg_type=666))
+    res = client.put("/ric/policies/control_admission_time", json=testing_helpers.good_payload())
+    assert res.status_code == 504
+    assert res.data == b"\"A1 was expecting an ACK back but it didn't receive one or didn't recieve the expected ACK\"\n"
+
+    # bad state
+    monkeypatch.setattr("a1.a1rmr._dequeue_all_waiting_messages", _fake_dequeue(monkeypatch, msg_state=666))
+    res = client.put("/ric/policies/control_admission_time", json=testing_helpers.good_payload())
+    assert res.status_code == 504
+    assert res.data == b"\"A1 was expecting an ACK back but it didn't receive one or didn't recieve the expected ACK\"\n"
+
+
+def test_xapp_put_good(client, monkeypatch):
+    """ test policy put good"""
+    _test_put_patch(monkeypatch)
+
+    # do a good one
+    monkeypatch.setattr("a1.a1rmr._dequeue_all_waiting_messages", _fake_dequeue(monkeypatch))
+    res = client.put("/ric/policies/control_admission_time", json=testing_helpers.good_payload())
+    assert res.status_code == 200
+    assert res.json == {"status": "SUCCESS", "foo": "bar"}
+
+
+def test_xapp_put_bad_send(client, monkeypatch):
+    """
+    Test bad send failures
+    """
+    testing_helpers.patch_all(monkeypatch)
+
+    monkeypatch.setattr("rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(10))
+    res = client.put("/ric/policies/control_admission_time", json=testing_helpers.good_payload())
+    assert res.status_code == 504
+    assert res.data == b'"A1 was unable to send a needed message to a downstream subscriber"\n'
+
+    monkeypatch.setattr("rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(5))
+    res = client.put("/ric/policies/control_admission_time", json=testing_helpers.good_payload())
+    assert res.status_code == 504
+    assert res.data == b'"A1 was unable to send a needed message to a downstream subscriber"\n'
+
+
+def test_bad_requests(client, monkeypatch):
+    """Test bad requests"""
+    testing_helpers.patch_all(monkeypatch)
+
+    # test a 404
+    res = client.put("/ric/policies/noexist", json=testing_helpers.good_payload())
+    assert res.status_code == 404
+
+    # bad media type
+    res = client.put("/ric/policies/control_admission_time", data="notajson")
+    assert res.status_code == 415
+
+    # test a PUT body against a poliucy not expecting one
+    res = client.put("/ric/policies/test_policy", json=testing_helpers.good_payload())
+    assert res.status_code == 400
+    assert res.data == b'"BODY SUPPLIED BUT POLICY HAS NO EXPECTED BODY"\n'
+
+
+def test_missing_manifest(client, monkeypatch):
+    """
+    test that we get a 500 with an approrpiate message on a missing manifest
+    """
+
+    def f():
+        raise exceptions.MissingManifest()
+
+    monkeypatch.setattr("a1.utils.get_ric_manifest", f)
+
+    res = client.put(
+        "/ric/policies/control_admission_time", json=testing_helpers.good_payload(), headers={"Content-Type": "text/plain"}
+    )
+    assert res.status_code == 500
+    assert res.data == b'"A1 was unable to find the required RIC manifest. report this!"\n'
+
+
+def test_missing_rmr(client, monkeypatch):
+    """
+    test that we get a 500 with an approrpiate message on a missing rmr rmr_string
+    """
+    testing_helpers.patch_all(monkeypatch, nonexisting_rmr=True)
+    res = client.put(
+        "/ric/policies/control_admission_time", json=testing_helpers.good_payload(), headers={"Content-Type": "text/plain"}
+    )
+    assert res.status_code == 500
+    assert res.data == b'"A1 does not have a mapping for the desired rmr string. report this!"\n'
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644 (file)
index 0000000..baa6b96
--- /dev/null
@@ -0,0 +1,74 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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
+import jsonschema
+from a1 import utils, exceptions
+import testing_helpers
+
+
+def test_bad_get_ric_manifest(monkeypatch):
+    """
+    testing missing manifest
+    """
+
+    def badopen(filename, mode):
+        raise FileNotFoundError()
+
+    monkeypatch.setattr("builtins.open", badopen)
+    with pytest.raises(exceptions.MissingManifest):
+        utils.get_ric_manifest()
+
+
+def test_bad_get_rmr_mapping(monkeypatch):
+    """
+    testing missing rmr mapping
+    """
+
+    def badopen(filename, mode):
+        raise FileNotFoundError()
+
+    monkeypatch.setattr("builtins.open", badopen)
+    with pytest.raises(exceptions.MissingRmrMapping):
+        utils._get_rmr_mapping_table()
+
+
+def test_good_get_ric_manifest(monkeypatch):
+    """
+    test get_ric_manifest
+    """
+    testing_helpers.patch_all(monkeypatch)
+    utils.get_ric_manifest()
+
+
+def test_good_get_rmr_mapping(monkeypatch):
+    """
+    testing getting the ric maping table
+    """
+    testing_helpers.patch_all(monkeypatch)
+    utils._get_rmr_mapping_table()
+
+
+def test_validate(monkeypatch):
+    """
+    test json validation wrapper
+    """
+    testing_helpers.patch_all(monkeypatch)
+    ricmanifest = utils.get_ric_manifest()
+    schema = ricmanifest["controls"][0]["message_receives_payload_schema"]
+    utils.validate_json(testing_helpers.good_payload(), schema)
+    with pytest.raises(jsonschema.exceptions.ValidationError):
+        utils.validate_json({"dc_admission_start_time": "10:00:00", "dc_admission_end_time": "nevergonnagiveyouup"}, schema)
diff --git a/tests/testing_helpers.py b/tests/testing_helpers.py
new file mode 100644 (file)
index 0000000..3cf94e2
--- /dev/null
@@ -0,0 +1,42 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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 os
+from rmr.rmr_mocks import rmr_mocks
+
+
+def _get_fixture_path(name):
+    cur_dir = os.path.dirname(os.path.realpath(__file__))
+    return "{0}/fixtures/{1}".format(cur_dir, name)
+
+
+def patch_all(monkeypatch, nonexisting_rmr=False):
+    rmr_mocks.patch_rmr(monkeypatch)
+
+    # patch manifest
+    man = json.loads(open(_get_fixture_path("ricmanifest.json"), "r").read())
+    if nonexisting_rmr:
+        man["controls"][0]["message_receives_rmr_type"] = "DARKNESS"
+    monkeypatch.setattr("a1.utils.get_ric_manifest", lambda: man)
+
+    # patch rmr mapping
+    mapping = open(_get_fixture_path("rmr_string_int_mapping.txt"), "r").readlines()
+    monkeypatch.setattr("a1.utils._get_rmr_mapping_table", lambda: mapping)
+
+
+def good_payload():
+    return {"dc_admission_start_time": "10:00:00", "dc_admission_end_time": "11:00:00"}
diff --git a/tox-integration.ini b/tox-integration.ini
new file mode 100644 (file)
index 0000000..e636aed
--- /dev/null
@@ -0,0 +1,36 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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 = int
+
+[testenv:int]
+whitelist_externals=
+    sleep
+    docker-compose
+    ab
+deps =
+    pytest-xdist
+    tavern
+changedir=integration_tests
+commands_pre=
+    docker-compose up --build -d
+    sleep 2
+commands=
+    pytest -n 2
+    ab -n 100 -c 10 -u putdata -T application/json http://localhost:10000/ric/policies/control_admission_time
+commands_post=
+    docker-compose down
diff --git a/tox.ini b/tox.ini
new file mode 100644 (file)
index 0000000..a89f0a6
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,38 @@
+# ==================================================================================
+#       Copyright (c) 2019 Nokia
+#       Copyright (c) 2018-2019 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 = py37,flake8
+
+[testenv]
+deps=
+    pytest
+    coverage
+    pytest-cov
+setenv =
+    LD_LIBRARY_PATH = /usr/local/lib/
+    RMR_RCV_RETRY_INTERVAL = 1
+    RMR_RETRY_TIMES = 2
+commands=pytest --verbose --cov {envsitepackagesdir}/a1  --cov-report html
+
+[testenv:flake8]
+basepython = python3.7
+skip_install = true
+deps = flake8
+commands = flake8 setup.py a1 tests
+
+[flake8]
+extend-ignore = E501