Updated function test environment

Added interface stub for MR

Added simulator monitor

Added function test and test cases

Added a server for catching callbacks from the agent

Added test cases for SNDC A1-Controller NB

README files are generally not updated. Will be made in later commits.

Change-Id: Ibf35f70b3e6f2d3f9c117aaf26ecdb03f80158f8
Issue-ID: NONRTRIC-154
Signed-off-by: BjornMagnussonXA <bjorn.magnusson@est.tech>
diff --git a/near-rt-ric-simulator/mrstub/.gitignore b/near-rt-ric-simulator/mrstub/.gitignore
new file mode 100644
index 0000000..4512d6f
--- /dev/null
+++ b/near-rt-ric-simulator/mrstub/.gitignore
@@ -0,0 +1 @@
+.tmp.json
\ No newline at end of file
diff --git a/near-rt-ric-simulator/mrstub/Dockerfile b/near-rt-ric-simulator/mrstub/Dockerfile
new file mode 100644
index 0000000..f1d16b7
--- /dev/null
+++ b/near-rt-ric-simulator/mrstub/Dockerfile
@@ -0,0 +1,28 @@
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2020 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+FROM python:3.7
+
+WORKDIR /usr/src/app
+
+COPY requirements.txt requirements.txt
+
+RUN pip install -r requirements.txt
+
+COPY mr.py mr.py
+
+CMD [ "python3", "-u", "./mr.py" ]
\ No newline at end of file
diff --git a/near-rt-ric-simulator/mrstub/README.md b/near-rt-ric-simulator/mrstub/README.md
new file mode 100644
index 0000000..6a69930
--- /dev/null
+++ b/near-rt-ric-simulator/mrstub/README.md
@@ -0,0 +1,47 @@
+## mrstub - stub interface to interact with the policy agent over Dmaap ##
+
+The mrstub is intended for function tests to simulate a message router.
+The mrstub exposes the read and write urls, used by the agent, as configured in consul.
+In addition, request messages can be fed to the mrstub and the response messages can be read by polling.
+
+
+### Control interface ###
+
+The control interface can be used by any test script.
+The following REST operations are available:
+
+>Send a message to MR<br>
+This method puts a request message in the queue for the agent to pick up. The returned correlationId (auto generated by the mrstub) is used when polling for the reposone message of this particular request.<br>
+```URI and parameters (GET): /send-request?operation=<GET|PUT|POST|DELETE>&url=<url>```<br><br>
+```response: <correlation-id> (http 200) or 400 for parameter error or 500 for other errors```
+
+>Receive a message response for MR for the included correlation id<br>
+The method is for polling of messages, returns immediately containing the received response (if any) for the supplied correlationId.<br>
+```URI and parameter, (GET): /receive-response?correlationId=<correlationid>```<br><br>
+```response: <json-array of 1 response> 200 or empty 204 or other errors 500```
+
+>Metrics - counters<br>
+There are a number of counters that can be read to monitor the message processing. Do a http GET on any of the current counters and an integer value will be returned with http response code 200.
+```/counter/requests_submitted``` - The total number of requests sent from the application<br>
+```/counter/requests_fetched``` - The total number of requests picked up by the agent<br>
+```/counter/responses_submitted``` - The total number of responses written by the agent<br>
+```/counter/responses_fetched``` - The total number of responses picked up by the application<br>
+```/counter/current_requests``` - The current number of requests waiting to be picked up by the agent<br>
+```/counter/current_responses``` - The current number of responses waiting to be picked up by the application<br>
+
+
+### Build and start ###
+
+>Build image<br>
+```docker build -t mrstub .```
+
+>Start the image<br>
+```docker run -it -p 3905:3905 mrstub```
+
+The script ```mrstub-build-start.sh``` do the above two steps in one go. This starts the stub container in stand-alone mode for basic test.<br>If the mrstub should be executed manually with the agent, replace docker run with this command to connect to the docker network with the correct service name (--name shall be the same as configured in consul for the read and write streams).
+```docker run -it -p 3905:3905 --network nonrtric-docker-net --name message-router mrstub```
+
+
+### Basic test ###
+
+Basic test is made with the script ```basic_test.sh``` which tests all the available urls with a subset of the possible operations. Use the script ```mrstub-build-start.sh``` to start the mrstub in a container first.
\ No newline at end of file
diff --git a/near-rt-ric-simulator/mrstub/basic_test.sh b/near-rt-ric-simulator/mrstub/basic_test.sh
new file mode 100755
index 0000000..13ea3a6
--- /dev/null
+++ b/near-rt-ric-simulator/mrstub/basic_test.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2020 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+# Automated test script for mrstub container
+
+# mr-stub port
+export PORT=3905
+
+# source function to do curl and check result
+. ../common/do_curl_function.sh
+
+echo "=== Stub hello world ==="
+RESULT="OK"
+do_curl GET / 200
+
+echo "=== Stub reset ==="
+RESULT="OK"
+do_curl GET /reset 200
+
+## Test with json response
+
+echo "=== Send a request ==="
+RESULT="*"
+#create payload
+echo "{\"data\": \"data-value\"}" > .tmp.json
+
+do_curl POST '/send-request?operation=PUT&url=/test' 200 .tmp.json
+#Save id for later
+CORRID=$body
+
+echo "=== Fetch a response, shall be empty ==="
+RESULT=""
+do_curl GET '/receive-response?correlationid='$CORRID 204
+
+echo "=== Fetch a request ==="
+RESULT="json:[{\"apiVersion\":\"1.0\",\"operation\":\"PUT\",\"correlationId\":\""$CORRID"\",\"originatorId\": \"849e6c6b420\",\"payload\":{\"data\": \"data-value\"},\"requestId\":\"23343221\", \"target\":\"policy-agent\", \"timestamp\":\"????\", \"type\":\"request\",\"url\":\"/test\"}]"
+do_curl GET '/events/A1-POLICY-AGENT-READ/users/policy-agent' 200
+
+echo "=== Send a json response ==="
+# Create minimal accepted response message
+echo "[{\"correlationId\": \""$CORRID"\", \"message\": {\"test\":\"testresponse\"}, \"status\": \"200\"}]" > .tmp.json
+RESULT="OK"
+do_curl POST /events/A1-POLICY-AGENT-WRITE 200 .tmp.json
+
+echo "=== Fetch a response ==="
+RESULT="{\"test\": \"testresponse\"}200"
+do_curl GET '/receive-response?correlationid='$CORRID 200
+
+### Test with plain text response
+
+echo "=== Send a request ==="
+RESULT="*"
+do_curl POST '/send-request?operation=GET&url=/test2' 200
+#Save id for later
+CORRID=$body
+
+echo "=== Fetch a response, shall be empty ==="
+RESULT=""
+do_curl GET '/receive-response?correlationid='$CORRID 204
+
+echo "=== Fetch a request ==="
+RESULT="json:[{\"apiVersion\":\"1.0\",\"operation\":\"GET\",\"correlationId\":\""$CORRID"\",\"originatorId\": \"849e6c6b420\",\"payload\":{},\"requestId\":\"23343221\", \"target\":\"policy-agent\", \"timestamp\":\"????\", \"type\":\"request\",\"url\":\"/test2\"}]"
+do_curl GET '/events/A1-POLICY-AGENT-READ/users/policy-agent' 200
+
+echo "=== Send a json response ==="
+# Create minimal accepted response message
+echo "[{\"correlationId\": \""$CORRID"\", \"message\": \"test2-response\", \"status\": \"200\"}]" > .tmp.json
+RESULT="OK"
+do_curl POST /events/A1-POLICY-AGENT-WRITE 200 .tmp.json
+
+echo "=== Fetch a response ==="
+RESULT="test2-response200"
+do_curl GET '/receive-response?correlationid='$CORRID 200
+
+echo "********************"
+echo "*** All tests ok ***"
+echo "********************"
diff --git a/near-rt-ric-simulator/mrstub/mr.py b/near-rt-ric-simulator/mrstub/mr.py
new file mode 100644
index 0000000..45f8cfe
--- /dev/null
+++ b/near-rt-ric-simulator/mrstub/mr.py
@@ -0,0 +1,265 @@
+
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2020 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+from flask import Flask, request
+from time import sleep
+import time
+import datetime
+import json
+from flask import Flask
+from flask import Response
+import traceback
+
+app = Flask(__name__)
+
+# list of messages to/from Dmaap
+msg_requests={}
+msg_responses={}
+
+# Server info
+HOST_IP = "0.0.0.0"
+HOST_PORT = 3905
+
+# Metrics vars
+cntr_msg_requests_submitted=0
+cntr_msg_requests_fetched=0
+cntr_msg_responses_submitted=0
+cntr_msg_responses_fetched=0
+
+# Request and response constants
+AGENT_WRITE_URL="/events/A1-POLICY-AGENT-WRITE"
+AGENT_READ_URL="/events/A1-POLICY-AGENT-READ/users/policy-agent"
+APP_WRITE_URL="/send-request"
+APP_READ_URL="/receive-response"
+MIME_TEXT="text/plain"
+MIME_JSON="application/json"
+CAUGHT_EXCEPTION="Caught exception: "
+SERVER_ERROR="Server error :"
+
+#I'm alive function
+@app.route('/',
+    methods=['GET'])
+def index():
+    return 'OK', 200
+
+
+# Helper function to create a Dmaap request message
+# args : <GET|PUT|DELETE> <correlation-id> <json-string-payload - may be None> <url>
+# response: json formatted string of a complete Dmaap message
+def create_message(operation, correlation_id, payload, url):
+    if (payload is None):
+        payload="{}"
+    time_stamp=datetime.datetime.utcnow()
+    msg = '{\"apiVersion\":\"1.0\",\"operation\":\"'+operation+'\",\"correlationId\":\"'+correlation_id+'\",\"originatorId\": \"849e6c6b420\",'
+    msg = msg + '\"payload\":'+payload+',\"requestId\":\"23343221\", \"target\":\"policy-agent\", \"timestamp\":\"'+str(time_stamp)+'\", \"type\":\"request\",\"url\":\"'+url+'\"}'
+    return msg
+
+
+### MR-stub interface, for MR control
+
+# Send a message to MR
+# URI and parameters (GET): /send-request?operation=<GET|PUT|POST|DELETE>&url=<url>
+# response: <correlation-id> (http 200) o4 400 for parameter error or 500 for other errors
+@app.route(APP_WRITE_URL,
+    methods=['PUT','POST'])
+def sendrequest():
+    global msg_requests
+    global cntr_msg_requests_submitted
+
+    try:
+
+        oper=request.args.get('operation')
+        if (oper is None):
+            print(APP_WRITE_URL+" parameter 'operation' missing")
+            return Response('Parameter operation missing in request', status=400, mimetype=MIME_TEXT)
+
+        url=request.args.get('url')
+        if (url is None):
+            print(APP_WRITE_URL+" parameter 'url' missing")
+            return Response('Parameter url missing in request', status=400, mimetype=MIME_TEXT)
+
+        if (oper != "GET" and oper != "PUT" and oper != "POST" and oper != "DELETE"):
+            print(APP_WRITE_URL+" parameter 'operation' need to be: DEL|PUT|POST|DELETE")
+            return Response('Parameter operation does not contain DEL|PUT|POST|DELETE in request', status=400, mimetype=MIME_TEXT)
+
+        print(APP_WRITE_URL+" operation="+oper+" url="+url)
+        correlation_id=str(time.time_ns())
+        payload=None
+        if (oper == "PUT") and (request.json is not None):
+            payload=json.dumps(request.json)
+
+        msg=create_message(oper, correlation_id, payload, url)
+        print(msg)
+        print(APP_WRITE_URL+" MSG(correlationid = "+correlation_id+"): " + json.dumps(json.loads(msg), indent=2))
+        msg_requests[correlation_id]=msg
+        cntr_msg_requests_submitted += 1
+        return Response(correlation_id, status=200, mimetype=MIME_TEXT)
+    except Exception as e:
+        print(APP_WRITE_URL+"-"+CAUGHT_EXCEPTION+" "+str(e) + " "+traceback.format_exc())
+        return Response(SERVER_ERROR+" "+str(e), status=500, mimetype=MIME_TEXT)
+
+# Receive a message response for MR for the included correlation id
+# URI and parameter, (GET): /receive-response?correlationid=<correlation-id>
+# response: <json-array of 1 response> 200 or empty 204 or other errors 500
+@app.route(APP_READ_URL,
+    methods=['GET'])
+def receiveresponse():
+    global msg_responses
+    global cntr_msg_responses_fetched
+
+    try:
+        id=request.args.get('correlationid')
+        if (id is None):
+            print(APP_READ_URL+" parameter 'correclationid' missing")
+            return Response('Parameter correlationid missing in json', status=500, mimetype=MIME_TEXT)
+
+        if (id in msg_responses):
+            answer=msg_responses[id]
+            del msg_responses[id]
+            print(APP_READ_URL+" response (correlationid="+id+"): " + answer)
+            cntr_msg_responses_fetched += 1
+            return Response(answer, status=200, mimetype=MIME_JSON)
+
+        print(APP_READ_URL+" - no messages (correlationid="+id+"): ")
+        return Response('', status=204, mimetype=MIME_JSON)
+    except Exception as e:
+        print(APP_READ_URL+"-"+CAUGHT_EXCEPTION+" "+str(e) + " "+traceback.format_exc())
+        return Response(SERVER_ERROR+" "+str(e), status=500, mimetype=MIME_TEXT)
+
+### Dmaap interface ###
+
+# Read messages stream. URI according to agent configuration.
+# URI, (GET): /events/A1-POLICY-AGENT-READ/users/policy-agent
+# response: 200 <json array of request messages>, or 500 for other errors
+@app.route(AGENT_READ_URL,
+    methods=['GET'])
+def events_read():
+    global msg_requests
+    global cntr_msg_requests_fetched
+    try:
+        msgs=''
+        for item in msg_requests:
+            if (len(msgs)>1):
+                msgs=msgs+','
+            msgs=msgs+msg_requests[item]
+            cntr_msg_requests_fetched += 1
+
+        msg_requests={}
+        msgs='['+msgs+']'
+        print(AGENT_READ_URL+" MSGs: "+json.dumps(json.loads(msgs), indent=2))
+        return Response(msgs, status=200, mimetype=MIME_JSON)
+    except Exception as e:
+        print(AGENT_READ_URL+"-"+CAUGHT_EXCEPTION+" "+str(e) + " "+traceback.format_exc())
+        return Response(SERVER_ERROR+" "+str(e), status=500, mimetype=MIME_TEXT)
+
+# Write messages stream. URI according to agent configuration.
+# URI and payload, (PUT or POST): /events/A1-POLICY-AGENT-WRITE <json array of response messages>
+# response: OK 200 or 400 for missing json parameters, 500 for other errors
+@app.route(AGENT_WRITE_URL,
+    methods=['PUT','POST'])
+def events_write():
+    global msg_responses
+    global cntr_msg_responses_submitted
+
+    try:
+        answer=request.json
+        print(AGENT_WRITE_URL+ " json=" + json.dumps(answer, indent=2))
+        for item in answer:
+            id=item['correlationId']
+            if (id is None):
+                print(AGENT_WRITE_URL+" parameter 'correlatonid' missing")
+                return Response('Parameter <correlationid> missing in json', status=400, mimetype=MIME_TEXT)
+            msg=item['message']
+            if (msg is None):
+                print(AGENT_WRITE_URL+" parameter 'msgs' missing")
+                return Response('Parameter >message> missing in json', status=400, mimetype=MIME_TEXT)
+            status=item['status']
+            if (status is None):
+                print(AGENT_WRITE_URL+" parameter 'status' missing")
+                return Response('Parameter <status> missing in json', status=400, mimetype=MIME_TEXT)
+            if isinstance(msg, list) or isinstance(msg, dict):
+                msg_str=json.dumps(msg)+status[0:3]
+            else:
+                msg_str=msg+status[0:3]
+            msg_responses[id]=msg_str
+            cntr_msg_responses_submitted += 1
+            print(AGENT_WRITE_URL+ " msg+status (correlationid="+id+") :" + str(msg_str))
+    except Exception as e:
+        print(AGENT_WRITE_URL+"-"+CAUGHT_EXCEPTION+" "+str(e) + " "+traceback.format_exc())
+        return Response(SERVER_ERROR+" "+str(e), status=500, mimetype=MIME_TEXT)
+
+    return Response('OK', status=200, mimetype=MIME_TEXT)
+
+
+### Functions for metrics read out ###
+
+@app.route('/counter/requests_submitted',
+    methods=['GET'])
+def requests_submitted():
+    return Response(str(cntr_msg_requests_submitted), status=200, mimetype=MIME_TEXT)
+
+@app.route('/counter/requests_fetched',
+    methods=['GET'])
+def requests_fetched():
+    return Response(str(cntr_msg_requests_fetched), status=200, mimetype=MIME_TEXT)
+
+@app.route('/counter/responses_submitted',
+    methods=['GET'])
+def responses_submitted():
+    return Response(str(cntr_msg_responses_submitted), status=200, mimetype=MIME_TEXT)
+
+@app.route('/counter/responses_fetched',
+    methods=['GET'])
+def responses_fetched():
+    return Response(str(cntr_msg_responses_fetched), status=200, mimetype=MIME_TEXT)
+
+@app.route('/counter/current_requests',
+    methods=['GET'])
+def current_requests():
+    return Response(str(len(msg_requests)), status=200, mimetype=MIME_TEXT)
+
+@app.route('/counter/current_responses',
+    methods=['GET'])
+def current_responses():
+    return Response(str(len(msg_responses)), status=200, mimetype=MIME_TEXT)
+
+### Admin ###
+
+# Reset all messsages and counters
+@app.route('/reset',
+    methods=['GET', 'POST', 'PUT'])
+def reset():
+    global cntr_msg_requests_submitted
+    global cntr_msg_requests_fetched
+    global cntr_msg_responses_submitted
+    global cntr_msg_responses_fetched
+    global msg_requests
+    global msg_responses
+
+    cntr_msg_requests_submitted=0
+    cntr_msg_requests_fetched=0
+    cntr_msg_responses_submitted=0
+    cntr_msg_responses_fetched=0
+    msg_requests={}
+    msg_responses={}
+    return Response('OK', status=200, mimetype=MIME_TEXT)
+
+### Main function ###
+
+if __name__ == "__main__":
+    app.run(port=HOST_PORT, host=HOST_IP)
diff --git a/near-rt-ric-simulator/mrstub/mrstub-build-start.sh b/near-rt-ric-simulator/mrstub/mrstub-build-start.sh
new file mode 100755
index 0000000..1a463dc
--- /dev/null
+++ b/near-rt-ric-simulator/mrstub/mrstub-build-start.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2020 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+#Builds the mrstub container and starts it in interactive mode
+
+docker build -t mrstub .
+
+docker run -it -p 3905:3905 mrstub
diff --git a/near-rt-ric-simulator/mrstub/requirements.txt b/near-rt-ric-simulator/mrstub/requirements.txt
new file mode 100644
index 0000000..1dabaa8
--- /dev/null
+++ b/near-rt-ric-simulator/mrstub/requirements.txt
@@ -0,0 +1,3 @@
+Flask==1.1.1
+
+