Create an external web server building RESTful API 01/8501/1
authorhalil.cakal <halil.cakal@est.tech>
Fri, 27 May 2022 14:04:29 +0000 (15:04 +0100)
committerhalil.cakal <halil.cakal@est.tech>
Wed, 8 Jun 2022 14:08:35 +0000 (15:08 +0100)
-A Python server building REST supports HTTP/HTTPS protocols
-Reviewed for patchset2
-patch set 3 : Create a secondary docker image and move external server
inside in it
-patch set 4 : Create Open API specs and move base API operations inside
in it, change json schema of the test_message to a basic json file
-patch set 5 : Implement set|reset forced response and forced delay
including negative unit tests as well as get API for a single policy.
Code review cleanup
-patch set 6 : Change API URL of admin functions, add a test case to
check if an request lasts for an amount of time, and  clean-up.
-patch set 7 : Remove container-tag.yaml.
-patch set 8 : Make forced delay test case parametric function which
expected delay time in seconds can be passed in.
-patch set 9 : Fix typo.
-patch set 10: Generate my own certificate and key.

Issue-ID: NONRTRIC-755
Change-Id: I0ee622ad82e18639e4eb2d4a02863904f9bf3201
Signed-off-by: halil.cakal <halil.cakal@est.tech>
19 files changed:
near-rt-ric-simulator/test/EXT_SRV/Dockerfile [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV/api/EXT_SRV_api.yaml [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV/certificate/cert.crt [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV/certificate/generate_cert_and_key.sh [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV/certificate/key.crt [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV/certificate/pass [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV/nginx.conf [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV/src/main.py [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV/src/maincommon.py [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV/src/payload_logging.py [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV/src/server.py [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV/src/start.sh [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV/src/var_declaration.py [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV_TEST/basic_test.sh [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV_TEST/build_and_start.sh [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV_TEST/jsonfiles/alpha_policy.json [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV_TEST/jsonfiles/beta_policy.json [new file with mode: 0644]
near-rt-ric-simulator/test/EXT_SRV_TEST/jsonfiles/forced_response.json [new file with mode: 0644]
near-rt-ric-simulator/test/common/elapse_time_curl.sh [new file with mode: 0644]

diff --git a/near-rt-ric-simulator/test/EXT_SRV/Dockerfile b/near-rt-ric-simulator/test/EXT_SRV/Dockerfile
new file mode 100644 (file)
index 0000000..84144d7
--- /dev/null
@@ -0,0 +1,47 @@
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2022 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+FROM python:3.8-slim-buster
+
+RUN pip install connexion[swagger-ui]
+
+#install nginx and curl
+RUN apt-get update && apt-get install -y nginx=1.14.* nginx-extras curl
+
+WORKDIR /usr/src/app
+
+COPY api api
+COPY nginx.conf nginx.conf
+COPY certificate /usr/src/app/cert
+COPY src src
+
+ARG user=nonrtric
+ARG group=nonrtric
+
+RUN groupadd $user && \
+    useradd -r -g $group $user
+RUN chown -R $user:$group /usr/src/app
+RUN chown -R $user:$group /var/log/nginx
+RUN chown -R $user:$group /var/lib/nginx
+RUN chown -R $user:$group /etc/nginx/conf.d
+RUN touch /var/run/nginx.pid
+RUN chown -R $user:$group /var/run/nginx.pid
+
+USER ${user}
+
+RUN chmod +x src/start.sh
+CMD src/start.sh
diff --git a/near-rt-ric-simulator/test/EXT_SRV/api/EXT_SRV_api.yaml b/near-rt-ric-simulator/test/EXT_SRV/api/EXT_SRV_api.yaml
new file mode 100644 (file)
index 0000000..d37db61
--- /dev/null
@@ -0,0 +1,205 @@
+openapi: 3.0.0
+info:
+  title: 'External Server for A1 simulator'
+  version: 2.0.0
+  description: |
+    External test server.
+    © 2022, O-RAN Alliance.
+    All rights reserved.
+externalDocs:
+  description: 'An external server building CRUD RestFUL APIs which is provisioned by A1 simulator. It will be a refrence point for the callouts'
+  url: 'https://www.testserver/specifications'
+servers:
+  - url: '{apiRoot}'
+    variables:
+      apiRoot:
+        default: 'https://testserver.com'
+paths:
+  '/a1policies':
+    get:
+      operationId: server.get_all_a1_policies
+      description: 'Get all a1 policies'
+      tags:
+      - All a1policies
+      responses:
+        200:
+          description: 'Array of all a1 policies'
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  "$ref": "#/components/schemas/A1PolicyObject"
+                minItems: 0
+        429:
+          "$ref": "#/components/responses/429-TooManyRequests"
+        503:
+          "$ref": "#/components/responses/503-ServiceUnavailable"
+  '/a1policy/{a1policyId}':
+    parameters:
+      - name: a1policyId
+        in: path
+        required: true
+        schema:
+          "$ref": "#/components/schemas/A1PolicyId"
+    get:
+      operationId: server.get_a1_policy
+      description: 'Query for an A1 policy'
+      tags:
+      - Single A1 Policy Object
+      responses:
+        200:
+          description: 'The requested A1 policy object'
+          content:
+            application/json:
+              schema:
+                "$ref": "#/components/schemas/A1PolicyObject"
+        404:
+          "$ref": "#/components/responses/404-NotFound"
+        409:
+          "$ref": "#/components/responses/409-Conflict"
+        429:
+          "$ref": "#/components/responses/429-TooManyRequests"
+        503:
+          "$ref": "#/components/responses/503-ServiceUnavailable"
+    put:
+      operationId: server.put_a1_policy
+      description: 'Create an A1 policy'
+      tags:
+      - Individual A1 policy Object
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              "$ref": "#/components/schemas/A1PolicyObject"
+      responses:
+        200:
+          description: 'The A1 policy was updated'
+          content:
+            application/json:
+              schema:
+                "$ref": "#/components/schemas/A1PolicyObject"
+        201:
+          description: 'The A1 policy was created'
+          content:
+            application/json:
+              schema:
+                "$ref": "#/components/schemas/A1PolicyObject"
+          headers:
+            Location:
+              description: 'Contains the URI of the created A1 policy'
+              required: true
+              schema:
+                type: string
+        400:
+          "$ref": "#/components/responses/400-BadRequest"
+        409:
+          "$ref": "#/components/responses/409-Conflict"
+        429:
+          "$ref": "#/components/responses/429-TooManyRequests"
+        503:
+          "$ref": "#/components/responses/503-ServiceUnavailable"
+        507:
+          "$ref": "#/components/responses/507-InsufficientStorage"
+
+    delete:
+      operationId: server.delete_a1_policy
+      description: 'Delete an A1 policy'
+      tags:
+      - Individual a1policy Object
+      responses:
+        204:
+          description: 'The A1 policy was deleted'
+        404:
+          "$ref": "#/components/responses/404-NotFound"
+        429:
+          "$ref": "#/components/responses/429-TooManyRequests"
+        503:
+          "$ref": "#/components/responses/503-ServiceUnavailable"
+
+components:
+  schemas:
+    #
+    # Representation objects
+    #
+    A1PolicyObject:
+      title: 'Title'
+      description: 'A generic A1 policy object'
+      type: object
+
+    ProblemDetails:
+      description: 'A problem detail to carry details in a HTTP response according to RFC 7807'
+      type: object
+      properties:
+        type:
+          type: string
+        title:
+          type: string
+        status:
+          type: number
+        detail:
+          type: string
+        instance:
+          type: string
+
+    #
+    # Simple data types
+    #
+    JsonSchema:
+      description: 'A JSON schema following http://json-schema.org/draft-07/schema'
+      type: object
+
+    A1PolicyId:
+      description: 'A1 policy identifier.'
+      type: string
+
+  responses:
+    400-BadRequest:
+      description: 'A1 policy not properly formulated or not related to the method'
+      content:
+        application/problem+json:
+          schema:
+            "$ref": "#/components/schemas/ProblemDetails"
+
+    404-NotFound:
+      description: 'No resource found at the URI'
+      content:
+        application/problem+json:
+          schema:
+            "$ref": "#/components/schemas/ProblemDetails"
+
+    405-MethodNotAllowed:
+      description: 'Method not allowed for the URI'
+      content:
+        application/problem+json:
+          schema:
+            "$ref": "#/components/schemas/ProblemDetails"
+
+    409-Conflict:
+      description: 'Request could not be processed in the current state of the resource'
+      content:
+        application/problem+json:
+          schema:
+            "$ref": "#/components/schemas/ProblemDetails"
+
+    429-TooManyRequests:
+      description: 'Too many requests have been sent in a given amount of time'
+      content:
+        application/problem+json:
+          schema:
+            "$ref": "#/components/schemas/ProblemDetails"
+
+    503-ServiceUnavailable:
+      description: 'The provider is currently unable to handle the request due to a temporary overload'
+      content:
+        application/problem+json:
+          schema:
+            "$ref": "#/components/schemas/ProblemDetails"
+
+    507-InsufficientStorage:
+      description: 'The method could not be performed on the resource because the provider is unable to store the representation needed to successfully complete the request'
+      content:
+        application/problem+json:
+          schema:
+            "$ref": "#/components/schemas/ProblemDetails"
diff --git a/near-rt-ric-simulator/test/EXT_SRV/certificate/cert.crt b/near-rt-ric-simulator/test/EXT_SRV/certificate/cert.crt
new file mode 100644 (file)
index 0000000..8e4e282
--- /dev/null
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIID+zCCAuOgAwIBAgIUdGlOkoRDHpVMWilOvEsiKhwP0U4wDQYJKoZIhvcNAQEL
+BQAwgYwxCzAJBgNVBAYTAklFMRIwEAYDVQQIDAlXRVNUTUVBVEgxEDAOBgNVBAcM
+B0FUSExPTkUxETAPBgNVBAoMCEVyaWNzc29uMQwwCgYDVQQLDANFU1QxETAPBgNV
+BAMMCGVzdC50ZWNoMSMwIQYJKoZIhvcNAQkBFhRoYWxpbC5jYWthbEBlc3QudGVj
+aDAeFw0yMjA2MDgxNDAzMjZaFw00OTEwMjMxNDAzMjZaMIGMMQswCQYDVQQGEwJJ
+RTESMBAGA1UECAwJV0VTVE1FQVRIMRAwDgYDVQQHDAdBVEhMT05FMREwDwYDVQQK
+DAhFcmljc3NvbjEMMAoGA1UECwwDRVNUMREwDwYDVQQDDAhlc3QudGVjaDEjMCEG
+CSqGSIb3DQEJARYUaGFsaWwuY2FrYWxAZXN0LnRlY2gwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQCz6jckM1gDW/cpj/w5lmw5IvMe8LGJIodhd1d4Sjg0
+qOBleTDlG7c9/qfzP2JBV7fDN/+nBYIvoRp3UckuJqj7KKU9ZUu7wJ9ffAixz3xa
+yfygWQF4IPfBlDqD9Pn5VULJteLIUxUv6Jx3Dz/c1lasAi2sNqOk5BckMT6xVrZQ
++sBcF+Qjo6b7mT0PsqvCmDeY/3QkmkKPzpuuEtsaQd6X3SKJ6yfVXArTPhrkePIU
+wUDm1epg5kbMtGdXrO9aJExib6pSRIswWVmWLL6oJLImVq7VvV9SD033KZY9qPyk
+R4OxMpN/VviDI9+gxCDLkdLDU/rGPKIj4JBctrCt9eExAgMBAAGjUzBRMB0GA1Ud
+DgQWBBQZAkRome+A3RsZ1fQbcl+TYfzIsDAfBgNVHSMEGDAWgBQZAkRome+A3RsZ
+1fQbcl+TYfzIsDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBR
+LlTvSe+qxpw0yblQ9htAbw6s8nuCuNMV4T3GxQZMklAXXej8IE36lOFLNMvCIBsb
+CPvjC34vADJDdNL6B/iWX+6h7zC6/QCtB3MvagMTxbZ+61uhrNqf/X+imVL8xbaK
+Hw20eooowqqbbRpmmTR9J20vTXv9xF1QiqPgLbv6R0yHxky4GTeO42VtR/I9k9qK
+VmC+7LM9BlTo1p/fHFfglt+/OdYgND0LGxLZkkNLZdX01KBqzE1xdn4z2MZxIkm8
+qXh7O/eDANHY2D/Uh9jF7mUGG1yaiJMrqsi8COE0BukI4xabAcxb8hSMvjHjMtDt
+8FtKa3bbjHAH9I/C11gZ
+-----END CERTIFICATE-----
diff --git a/near-rt-ric-simulator/test/EXT_SRV/certificate/generate_cert_and_key.sh b/near-rt-ric-simulator/test/EXT_SRV/certificate/generate_cert_and_key.sh
new file mode 100644 (file)
index 0000000..7e6d29c
--- /dev/null
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2022 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+# This will generate a self-signed certificate with password 'test'
+
+SUBJECT="/C=IE/ST=WESTMEATH/L=ATHLONE/O=Ericsson/OU=EST/CN=est.tech/emailAddress=halil.cakal@est.tech"
+PW=test
+echo $PW > pass
+
+openssl req -x509 -passout file:pass -newkey rsa:2048 -keyout key.crt -subj "$SUBJECT" -out cert.crt -days 9999
diff --git a/near-rt-ric-simulator/test/EXT_SRV/certificate/key.crt b/near-rt-ric-simulator/test/EXT_SRV/certificate/key.crt
new file mode 100644 (file)
index 0000000..0804e6d
--- /dev/null
@@ -0,0 +1,30 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIX3ACKBAs5wUCAggA
+MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECPPoseIGFm4PBIIEyA/wL1c+zb6s
+ucyML47Ye9Xe4CFu1hzKRQ28BJlC/JKaPC0vn2JEwn1ZpuPy/Tiy3DoPiUkENvLQ
+OgKBgjZdPGj1kv8dcWBg45RAo8gKw2uuw/8R29K+Ch44JTaTRdnYolky5ZijiyJN
+i5MG5h7BNCHZg+eAat+o8NgNHCMoaVLN2q5NM5jlryoLUj0hUL/SQsx7FUfkDBX4
+VksLUaJgCTHiU4EPh1qPH/oh6vk8qYRw3Y9S7ot7Ghccbnzd3eHyflwmQROt7jGg
+ufe9beVx43yjNzM5nm5W6W1xYPhRpVeFIlTZz4+UPEC0IaC/N8gR5gXPMfC61aYq
+BlXq7XQxGqQokUtOpOBC3B1avflvTdQcP8L14Mo8BmHUKMoJAjC/dO3Rl932f+Rv
+UWSprGKJIADr8BCYAIiVwCMa4sXefmXg+IeFNrTyKcR9Ew2bn2AbUy4GQjHVW78l
+21cuc2VgnJq1+TqpJWkkJ95qcGRHB3JqbVdSsHLktkSa8mn/kn0TgkgmxCkAqcnz
+/9/F1VNIpQ2NveVpBVVBk4yFudHkvJ3D1mJ1aDnimPe830pB5eSWnnM2ZS/U8xsc
+3HDx0P36JqpoHdzdSrUx5fIE+rfIkhiSfb1uolurcVWlIb4AP3pVepWmQv9WGswP
+AVdX80Mdv4sAWPWwxOHOienR2SWYy0pHxEmyNx3NdpAD/rDsJW6quMoauCtHLy1w
+9nPh3mJrhi+AmAYyyZKdvTQhBj7JInwVX40uAnxuhIuwj1CcxOLNhFLOX6WLhNan
+Boc9QveAXzY5PVq2crdR3zPxQRMPAbp2/DQ4VeMfLZbkNxvG0ATm7LqygMu51/q6
+OT0KgIAn+602Q7ncOdYj4PKqSO05BzNVWtWqRT1DxIg5cZbisNJo51X1AIW0BTlh
+lhgbtHmwB80T367XPTSMp2gWjUVikSPO3+qc8pQEqqxFe5BsO3ueN2QoqzR77YbO
+2CjXX7IXLxdqQhZJjiFLv8FbFDEG0dw2cNwmFw2fHSdCL0q0DYZK0glQv1QDtDTj
+eiHO3pMSrvqB4e3EmitoskqPn/e+jjOsv9ETrTvOM0x2RftAm4MBozff7DqKs+uR
+rQfAk5/HzRq0ARlw/xM8BoSd6lwV61bGegaH/+X/y5EU1Cy0hwZqhww1cT/DdGgJ
+ukga3f8J7W/eNOHZzse2X1koBr4LarXgZigQGsW21V7Rg5HX6sAyBJGNcg61pBdi
+SNJz2O76jUMNtZ1y3UvLZiuGcLPrQXCpm21EaMWWLbH6tSiz03r657DXIjtdN/Li
+uoBqznQJ/EwYeicA+cFQkjbszxB96aVxM88fpGigOU6mWVYsi3/5DBDVSINtrzVh
+0oHK0lar4iPuXNw9eQkk52MhQqHqCg5jtsiIGlVAFWDlVKnvGKoyVWzKmS3UsGWE
+LDLpdvIHVTXMBaNwqLn6+7Tk6wgJ0OMAdc1CCJ+iGyLXnV3KIPJzMmit/aFHlhnq
+tEFMvW+ghl1DyMTJUxgoGg246gttRgRTP0iR761A+mOAxo3Vdk8GoVUU2MI/VACe
+WTYOIIwBwv1VK0j5O07hWyKL4ieoaMseJqplJOslfsAFb8pY7bvCsaN9tkhYhuHY
+VfctyAvMEN0MWZO/g9vDsA==
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/near-rt-ric-simulator/test/EXT_SRV/certificate/pass b/near-rt-ric-simulator/test/EXT_SRV/certificate/pass
new file mode 100644 (file)
index 0000000..9daeafb
--- /dev/null
@@ -0,0 +1 @@
+test
diff --git a/near-rt-ric-simulator/test/EXT_SRV/nginx.conf b/near-rt-ric-simulator/test/EXT_SRV/nginx.conf
new file mode 100644 (file)
index 0000000..7b3e620
--- /dev/null
@@ -0,0 +1,93 @@
+# user www-data;
+worker_processes auto;
+pid /run/nginx.pid;
+include /etc/nginx/modules-enabled/*.conf;
+
+env ALLOW_HTTP;
+
+events {
+    worker_connections 768;
+    # multi_accept on;
+}
+
+http {
+
+    ##
+    # Basic Settings
+    ##
+
+    sendfile on;
+    tcp_nopush on;
+    tcp_nodelay on;
+    keepalive_timeout 65;
+    types_hash_max_size 2048;
+    # server_tokens off;
+
+    # server_names_hash_bucket_size 64;
+    # server_name_in_redirect off;
+
+    include /etc/nginx/mime.types;
+    default_type application/octet-stream;
+
+    perl_set $allow_http 'sub { return $ENV{"ALLOW_HTTP"}; }';
+
+    server { # simple reverse-proxy
+       listen      8085;
+        listen      [::]:8085;
+        server_name  localhost;
+       if ($allow_http != true) {
+           return 444;
+       }
+
+       # serve dynamic requests
+        location / {
+            proxy_set_header   Host                 $host;
+            proxy_set_header   X-Real-IP            $remote_addr;
+            proxy_set_header   X-Forwarded-For      $proxy_add_x_forwarded_for;
+            proxy_pass      http://localhost:2222;
+        }
+    }
+
+    server { # simple reverse-proxy
+        listen      8185 ssl;
+        listen      [::]:8185 ssl;
+        server_name  localhost;
+        ssl_certificate     /usr/src/app/cert/cert.crt;
+        ssl_certificate_key /usr/src/app/cert/key.crt;
+        ssl_password_file   /usr/src/app/cert/pass;
+
+        # serve dynamic requests
+        location / {
+            proxy_set_header   Host                 $host;
+            proxy_set_header   X-Real-IP            $remote_addr;
+            proxy_set_header   X-Forwarded-For      $proxy_add_x_forwarded_for;
+            proxy_pass      http://localhost:2222;
+        }
+    }
+    ##
+    # SSL Settings
+    ##
+
+    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
+    ssl_prefer_server_ciphers on;
+
+    ##
+    # Logging Settings
+    ##
+
+    access_log /var/log/nginx/access.log;
+    error_log /var/log/nginx/error.log;
+
+    ##
+    # Gzip Settings
+    ##
+
+    gzip on;
+
+    # gzip_vary on;
+    # gzip_proxied any;
+    # gzip_comp_level 6;
+    # gzip_buffers 16 8k;
+    # gzip_http_version 1.1;
+    # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+}
\ No newline at end of file
diff --git a/near-rt-ric-simulator/test/EXT_SRV/src/main.py b/near-rt-ric-simulator/test/EXT_SRV/src/main.py
new file mode 100644 (file)
index 0000000..4323d10
--- /dev/null
@@ -0,0 +1,82 @@
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2022 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+import json
+import sys
+import requests
+
+
+from flask import request, Response, Flask, json
+from var_declaration import a1_policy_instances, forced_settings, app
+from maincommon import check_apipath
+
+#Constants
+TEXT_PLAIN='text/plain'
+
+check_apipath()
+
+# app is created in var_declarations
+
+import payload_logging   # app var need to be initialized
+
+#Check alive function
+@app.route('/', methods=['GET'])
+def test():
+  return Response("OK", 200, mimetype=TEXT_PLAIN)
+
+#Delete all created instances and status
+@app.route('/serveradmin/deleteinstances', methods=['POST'])
+def delete_instances():
+    a1_policy_instances.clear()
+    return Response("All a1 policy instances deleted", 200, mimetype=TEXT_PLAIN)
+
+#Set|Reset force response to be returned from external server
+#/a1policy/forceresponse?code=<responsecode>
+@app.route('/serveradmin/forceresponse', methods=['POST'])
+def forceresponse():
+
+  query_param=request.args.get('code')
+  forced_settings['code']=query_param
+
+  if (query_param is None):
+    return Response("Force response code has been resetted for all external server responses", 200, mimetype=TEXT_PLAIN)
+  else:
+    return Response("Force response code: " + str(forced_settings['code']) + " set for all external server response until it is resetted", 200, mimetype=TEXT_PLAIN)
+
+#Set|Reset force delay response, in seconds, for all external server responses
+#/a1policy/forcedelay?delay=<seconds>
+@app.route('/serveradmin/forcedelay', methods=['POST'])
+def forcedelay():
+
+  query_param=request.args.get('delay')
+  forced_settings['delay']=query_param
+
+  if (query_param is None):
+    return Response("Force delay has been resetted for all external server responses ", 200, mimetype=TEXT_PLAIN)
+  else:
+    return Response("Force delay: " + str(forced_settings['delay']) + " sec set for all external server responses until it is resetted ", 200, mimetype=TEXT_PLAIN)
+
+port_number = 2222
+if len(sys.argv) >= 2:
+  if isinstance(sys.argv[1], int):
+    port_number = sys.argv[1]
+
+#Import base RestFUL API functions from Open API
+app.add_api('EXT_SRV_api.yaml')
+
+if __name__ == '__main__':
+  app.run(port=port_number, host="127.0.0.1", threaded=False)
diff --git a/near-rt-ric-simulator/test/EXT_SRV/src/maincommon.py b/near-rt-ric-simulator/test/EXT_SRV/src/maincommon.py
new file mode 100644 (file)
index 0000000..9dd0a0f
--- /dev/null
@@ -0,0 +1,34 @@
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2021 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+import os
+import sys
+from pathlib import Path
+from flask import Response
+import socket
+import ssl
+
+#Must exist
+apipath=os.environ['APIPATH']
+#May exist
+remote_hosts_logging=os.getenv('REMOTE_HOSTS_LOGGING')
+
+# Make sure the api path for the interface yaml file is set, otherwise exit
+def check_apipath():
+    if (apipath is None):
+        print("Env APIPATH not set. Exiting....")
+        sys.exit(1)
diff --git a/near-rt-ric-simulator/test/EXT_SRV/src/payload_logging.py b/near-rt-ric-simulator/test/EXT_SRV/src/payload_logging.py
new file mode 100644 (file)
index 0000000..9457d04
--- /dev/null
@@ -0,0 +1,60 @@
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2022 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+from var_declaration import app
+from flask import Flask, request, Response
+
+#Constants
+TEXT_PLAIN='text/plain'
+
+#Vars
+payload_log=True
+
+#Function to activate/deactivate http header and payload logging
+@app.route('/payload_logging/<state>', methods=['POST', 'PUT'])
+def set_payload_logging(state):
+  global payload_log
+  if (state == "on"):
+    payload_log=True
+  elif (state == "off"):
+    payload_log=False
+  else:
+    return Response("Unknown state: "+state+" - use 'on' or 'off'", 400, mimetype=TEXT_PLAIN)
+
+  return Response("Payload and header logging set to: "+state, 200, mimetype=TEXT_PLAIN)
+
+# Generic function to log http header and payload - called before the request
+@app.app.before_request
+def log_request_info():
+    if (payload_log is True):
+        print('')
+        print('-----Request-----')
+        print('Req Headers: ', request.headers)
+        print('Req Body: ', request.get_data())
+
+# Generic function to log http header and payload - called after the response
+@app.app.after_request
+def log_response_info(response):
+    if (payload_log is True):
+        print('-----Response-----')
+        print('Resp Headers: ', response.headers)
+        print('Resp Body: ', response.get_data())
+    return response
+
+# Helper function to check loggin state
+def is_payload_logging():
+  return payload_log
diff --git a/near-rt-ric-simulator/test/EXT_SRV/src/server.py b/near-rt-ric-simulator/test/EXT_SRV/src/server.py
new file mode 100644 (file)
index 0000000..77286be
--- /dev/null
@@ -0,0 +1,164 @@
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2022 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+import copy
+import datetime
+import json
+import logging
+import collections
+import time
+
+from flask import Flask, escape, request, Response, make_response
+from jsonschema import validate
+from var_declaration import a1_policy_instances, forced_settings
+
+
+#Constsants
+APPL_JSON='application/json'
+APPL_PROB_JSON='application/problem+json'
+
+#Python implementation of EXT_SRV_api.yaml - Open API -
+
+# API Function: Get all a1 policy ids
+def get_all_a1_policies():
+
+  if ((r := check_modified_response()) is not None):
+    return r
+
+  res = list(a1_policy_instances)
+  return (res, 200)
+
+# API Function: Get A1 policy
+def get_a1_policy(a1policyId):
+
+  if ((r := check_modified_response()) is not None):
+    return r
+
+  a1_policy_id=str(a1policyId)
+
+  policykeys=a1_policy_instances.keys()
+  if (a1_policy_id not in policykeys):
+    pjson=create_problem_json(None, "The A1 policy requested does not exist.", 404, None, a1_policy_id)
+    return Response(json.dumps(pjson), 404, mimetype=APPL_PROB_JSON)
+
+  return Response(json.dumps(a1_policy_instances[a1_policy_id]), 200, mimetype=APPL_JSON)
+
+# API Function: Create or update a a1policy
+def put_a1_policy(a1policyId):
+
+  if ((r := check_modified_response()) is not None):
+    return r
+
+  a1_policy_id=str(a1policyId)
+
+  try:
+    data = request.data
+    data = json.loads(data)
+  except Exception:
+    pjson=create_problem_json(None, "The a1policy is corrupt or missing.", 400, None, a1_policy_id)
+    return Response(json.dumps(pjson), 400, mimetype=APPL_PROB_JSON)
+
+  policykeys=a1_policy_instances.keys()
+
+  retcode=201
+  if a1_policy_id in policykeys:
+    retcode=200
+
+  a1_policy_instances[a1_policy_id]=data
+
+  if (retcode == 200):
+    return Response(json.dumps(data), 200, mimetype=APPL_JSON)
+  else:
+    headers={}
+    headers['Location']='/a1policy/' + a1_policy_id
+    return Response(json.dumps(data), 201, headers=headers, mimetype=APPL_JSON)
+
+# API Function: Delete a a1policy
+def delete_a1_policy(a1policyId):
+
+  if ((r := check_modified_response()) is not None):
+    return r
+
+  a1_policy_id=str(a1policyId)
+
+  policykeys=a1_policy_instances.keys()
+  if (a1_policy_id not in policykeys):
+    pjson=create_problem_json(None, "The a1policy does not exist.", 404, None, a1_policy_id)
+    return Response(json.dumps(pjson), 404, mimetype=APPL_PROB_JSON)
+
+  del a1_policy_instances[a1_policy_id]
+  return Response('', 204, mimetype=APPL_JSON)
+
+
+# Helper: Create a response object if forced http response code is set
+def get_forced_response():
+  if (forced_settings['code'] is not None):
+    pjson=create_error_response(forced_settings['code'])
+    return Response(json.dumps(pjson), pjson['status'], mimetype=APPL_PROB_JSON)
+  return None
+
+
+# Helper: Delay if delayed response code is set
+def do_delay():
+  if (forced_settings['delay'] is not None):
+    try:
+      val=int(forced_settings['delay'])
+      time.sleep(val)
+    except Exception:
+      return
+
+
+# Helper: Check if response shall be delayed or a forced response shall be sent
+def check_modified_response():
+  do_delay()
+  return get_forced_response()
+
+
+# Helper: Create a problem json object
+def create_problem_json(type_of, title, status, detail, instance):
+  error = {}
+  if type_of is not None:
+    error["type"] = type_of
+  if title is not None:
+    error["title"] = title
+  if status is not None:
+    error["status"] = status
+  if detail is not None:
+    error["detail"] = detail
+  if instance is not None:
+    error["instance"] = instance
+  return error
+
+
+# Helper: Create a problem json based on a generic http response code
+def create_error_response(code):
+    if code == 400:
+      return(create_problem_json(None, "Bad request", 400, "Object in payload not properly formulated or not related to the method", None))
+    elif code == 404:
+      return(create_problem_json(None, "Not found", 404, "No resource found at the URI", None))
+    elif code == 405:
+      return(create_problem_json(None, "Method not allowed", 405, "Method not allowed for the URI", None))
+    elif code == 409:
+      return(create_problem_json(None, "Conflict", 409, "Request could not be processed in the current state of the resource", None))
+    elif code == 429:
+      return(create_problem_json(None, "Too many requests", 429, "Too many requests have been sent in a given amount of time", None))
+    elif code == 507:
+      return(create_problem_json(None, "Insufficient storage", 507, "The method could not be performed on the resource because the provider is unable to store the representation needed to successfully complete the request", None))
+    elif code == 503:
+      return(create_problem_json(None, "Service unavailable", 503, "The provider is currently unable to handle the request due to a temporary overload", None))
+    else:
+      return(create_problem_json(None, "Unknown", code, "Not implemented response code", None))
diff --git a/near-rt-ric-simulator/test/EXT_SRV/src/start.sh b/near-rt-ric-simulator/test/EXT_SRV/src/start.sh
new file mode 100644 (file)
index 0000000..052a8e4
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2022 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+#Set path to open api
+export APIPATH=$PWD/api
+echo "APIPATH set to: "$APIPATH
+
+cd src
+
+#start nginx
+nginx -c /usr/src/app/nginx.conf
+
+#start near-rt-ric-simulator
+echo "Path to main.py: "$PWD
+python -u main.py
diff --git a/near-rt-ric-simulator/test/EXT_SRV/src/var_declaration.py b/near-rt-ric-simulator/test/EXT_SRV/src/var_declaration.py
new file mode 100644 (file)
index 0000000..1f40ec1
--- /dev/null
@@ -0,0 +1,27 @@
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2022 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+from maincommon import apipath
+import connexion
+
+#Main app
+app = connexion.App(__name__, specification_dir=apipath)
+
+a1_policy_instances = {}
+forced_settings = {}
+forced_settings['code']=None
+forced_settings['delay']=None
diff --git a/near-rt-ric-simulator/test/EXT_SRV_TEST/basic_test.sh b/near-rt-ric-simulator/test/EXT_SRV_TEST/basic_test.sh
new file mode 100644 (file)
index 0000000..14490be
--- /dev/null
@@ -0,0 +1,146 @@
+#!/bin/bash
+
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2022 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+# Script for basic test of the simulator.
+# Run the build_and_start with the same arg, except arg 'nonsecure|secure', as this script
+
+print_usage() {
+    echo "Usage: ./basic_test.sh nonsecure|secure "
+    exit 1
+}
+
+if [ $# -ne 1 ]; then
+    print_usage
+fi
+if [ "$1" != "nonsecure" ] && [ "$1" != "secure" ]; then
+    print_usage
+fi
+
+if [ $1 == "nonsecure" ]; then
+    #Default http port for the simulator
+    PORT=8085
+    # Set http protocol
+    HTTPX="http"
+else
+    #Default https port for the simulator
+    PORT=8185
+    # Set https protocol
+    HTTPX="https"
+fi
+
+. ../common/test_common.sh
+. ../common/elapse_time_curl.sh
+
+echo "=== Simulator hello world ==="
+RESULT="OK"
+do_curl GET / 200
+
+echo "=== Reset simulator a1policy instances ==="
+RESULT="All a1 policy instances deleted"
+do_curl POST /serveradmin/deleteinstances 200
+
+echo "=== Reset force delay ==="
+RESULT="Force delay has been resetted for all external server responses"
+do_curl POST /serveradmin/forcedelay 200
+
+echo "=== Put an a1 policy: alpha ==="
+res=$(cat jsonfiles/alpha_policy.json)
+RESULT="json:$res"
+do_curl PUT  /a1policy/alpha 201 jsonfiles/alpha_policy.json
+
+echo "=== Get an a1 policy: alpha ==="
+res=$(cat jsonfiles/alpha_policy.json)
+RESULT="json:$res"
+do_curl GET /a1policy/alpha 200
+
+echo "=== Put an a1 policy alpha to update ==="
+res=$(cat jsonfiles/alpha_policy.json)
+RESULT="json:$res"
+do_curl PUT  /a1policy/alpha 200 jsonfiles/alpha_policy.json
+
+echo "=== API: Get a1 policy ids, shall contain a1policy alpha ==="
+RESULT="json:[\"alpha\"]"
+do_curl GET /a1policies 200
+
+echo "=== Delete an a1 policy: alpha ==="
+RESULT=""
+do_curl DELETE  /a1policy/alpha 204
+
+echo "=== Get an a1 policy: alpha, A1 policy instance not found ==="
+RESULT="json:{\"title\": \"The A1 policy requested does not exist.\", \"status\": 404, \"instance\": \"alpha\"}"
+do_curl GET /a1policy/alpha 404
+
+echo "=== API: Get a1 policies, shall be empty ==="
+RESULT="json:[]"
+do_curl GET  /a1policies 200
+
+echo "=== Set force delay 5 sec ==="
+RESULT="Force delay: 5 sec set for all external server responses until it is resetted"
+do_curl POST '/serveradmin/forcedelay?delay=5' 200
+
+echo "=== API: Get a1 policies, should respond after 5 seconds later ==="
+RESULT="json:[]"
+do_curl GET  /a1policies 200
+
+echo "=== API: Get A1 policy ids, shall wait at least <delay-time> sec and then respond ==="
+RESULT="json:[]"
+do_elapsetime_curl GET  /a1policies 200 5
+
+echo "=== Reset force delay ==="
+RESULT="Force delay has been resetted for all external server responses"
+do_curl POST /serveradmin/forcedelay 200
+
+echo "=== Put an a1 policy: beta ==="
+res=$(cat jsonfiles/beta_policy.json)
+RESULT="json:$res"
+do_curl PUT  /a1policy/beta 201 jsonfiles/beta_policy.json
+
+echo "=== Put an a1 policy: alpha ==="
+res=$(cat jsonfiles/alpha_policy.json)
+RESULT="json:$res"
+do_curl PUT  /a1policy/alpha 201 jsonfiles/alpha_policy.json
+
+echo "=== API: Get a1 policy ids, shall contain a1policy beta and alpha ==="
+RESULT="json:[\"beta\", \"alpha\"]"
+do_curl GET /a1policies 200
+
+echo "=== Set force response code: 500 ==="
+RESULT="Force response code: 500 set for all external server response until it is resetted"
+do_curl POST  '/serveradmin/forceresponse?code=500' 200
+
+echo "=== API: Get a1 policies, shall return reponse code 500 =="
+res=$(cat jsonfiles/forced_response.json)
+RESULT="json:$res"
+do_curl GET  /a1policies 500
+
+echo "=== Reset force response code ==="
+RESULT="Force response code has been resetted for all external server responses"
+do_curl POST  /serveradmin/forceresponse 200
+
+echo "=== Delete an a1policy: alpha ==="
+RESULT=""
+do_curl DELETE  /a1policy/alpha 204
+
+echo "=== API: Get a1policy ids, shall contain a1 policy beta ==="
+RESULT="json:[\"beta\"]"
+do_curl GET /a1policies 200
+
+echo "********************"
+echo "*** All tests ok ***"
+echo "********************"
diff --git a/near-rt-ric-simulator/test/EXT_SRV_TEST/build_and_start.sh b/near-rt-ric-simulator/test/EXT_SRV_TEST/build_and_start.sh
new file mode 100644 (file)
index 0000000..4deea51
--- /dev/null
@@ -0,0 +1,45 @@
+#!/bin/bash
+
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2022 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+# Script to build and start the container
+# Make sure to run the simulator with the same arg as this script
+
+print_usage() {
+    echo "Usage: ./build_and_start.sh "
+    exit 1
+}
+
+if [ $# -ge 1 ]; then
+    print_usage
+fi
+
+echo "Building external server image..."
+cd ../EXT_SRV/
+
+#Build the image
+docker build -t external_server .
+
+docker stop externalserversimulator > /dev/null 2>&1
+docker rm -f externalserversimulator > /dev/null 2>&1
+
+echo "Starting external server for A1 simulator callouts..."
+echo "PWD path: "$PWD
+
+#Run the container in interactive mode, unsecure port 8085, secure port 8185
+docker run --rm -it -p 8085:8085 -p 8185:8185 -e ALLOW_HTTP=true --volume "$PWD/certificate:/usr/src/app/cert" --name externalserversimulator external_server
diff --git a/near-rt-ric-simulator/test/EXT_SRV_TEST/jsonfiles/alpha_policy.json b/near-rt-ric-simulator/test/EXT_SRV_TEST/jsonfiles/alpha_policy.json
new file mode 100644 (file)
index 0000000..66c2b63
--- /dev/null
@@ -0,0 +1,11 @@
+{
+
+          "title": "A1 policy external server",
+          "description": "A1 policies notifying external server",
+          "type": "object",
+          "properties": {
+            "a1policyType": "alpha_test_policy",
+            "url" : "http://www.com"
+          }
+
+}
diff --git a/near-rt-ric-simulator/test/EXT_SRV_TEST/jsonfiles/beta_policy.json b/near-rt-ric-simulator/test/EXT_SRV_TEST/jsonfiles/beta_policy.json
new file mode 100644 (file)
index 0000000..a61c7fc
--- /dev/null
@@ -0,0 +1,11 @@
+{
+
+          "title": "A1 policy external server",
+          "description": "A1 policies notifying external server",
+          "type": "object",
+          "properties": {
+            "a1policyType": "beta_test_policy",
+            "url" : "http://www.com"
+          }
+
+}
diff --git a/near-rt-ric-simulator/test/EXT_SRV_TEST/jsonfiles/forced_response.json b/near-rt-ric-simulator/test/EXT_SRV_TEST/jsonfiles/forced_response.json
new file mode 100644 (file)
index 0000000..7fb5ff2
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "title": "Unknown",
+    "status": "500",
+    "detail": "Not implemented response code"
+}
diff --git a/near-rt-ric-simulator/test/common/elapse_time_curl.sh b/near-rt-ric-simulator/test/common/elapse_time_curl.sh
new file mode 100644 (file)
index 0000000..618ad42
--- /dev/null
@@ -0,0 +1,93 @@
+#!/bin/bash
+
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2022 Nordix Foundation. All rights reserved.
+#  ========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END=================================================
+#
+
+# Function to execute curl and compare + print result
+
+# Note: Env var PORT must be set to the intended port number
+# Notre Env var HTTPX must be set to either 'http' or 'https'
+
+#args: <http-operation> <url> <response-code> [file]
+#Expects the env $RESULT to contain the expected RESULT.
+#If json, the RESULT shall begin with 'json:'.
+#Any json parameter with unknown value shall be given as "????" to skip checking the value.
+do_elapsetime_curl() {
+    if [ $# -lt 4 ]; then
+        echo "Need 4 or more parameters, <http-operation> <url> <response-code> <delay-time> [file]: "$@
+        echo "Exiting test script....."
+        exit 1
+    fi
+
+    #capture total elapsetime via ~%{time_total}
+    curlstr="curl -X "$1" -skw %{http_code}~%{time_total} $HTTPX://localhost:"${PORT}${2}" -H accept:*/*"
+    if [ $# -gt 4 ]; then
+        curlstr=$curlstr" -H Content-Type:application/json --data-binary @"$4
+    fi
+    echo "  CMD (${BASH_LINENO[0]}):"$curlstr
+    res=$($curlstr)
+
+    #target delay time for a single request
+    delay_time=$4
+
+    #extract time_total and get rid of its decimals points
+    left=$(echo $res | cut -d'~' -f1)
+    right=$(echo $res | cut -d'~' -f2)
+    elapsedtime=$(echo $right | cut -d'.' -f1)
+
+    res=$left
+    status=${res:${#res}-3}
+    body=${res:0:${#res}-3}
+
+    if [ $status -ne $3 ]; then
+        echo "  Error status :"$status" Expected status: "$3
+        echo "  Body         :"$body
+        echo "Exiting test script....."
+        exit 1
+    elif [ $elapsedtime -lt $delay_time ]; then
+        echo "  Elapsed time :"$elapsedtime" Expected delay time:"$delay_time "seconds"
+        echo "Exiting test script....."
+        exit 1
+    else
+        echo "  Delay OK           :"$elapsedtime"     (Expected)"
+        echo "  Body               :"$body
+        if [ "$RESULT" == "*" ]; then
+            echo "  Body contents not checked"
+        elif [[ "$RESULT" == "json:"* ]]; then
+            result=${RESULT:5:${#RESULT}} #Remove 'json:' from the result string
+            res=$(python ../common/compare_json.py "$result" "$body")
+            if [ $res -eq 0 ]; then
+                echo "  Expected json body :"$result
+                echo "  Body as expected"
+            else
+                echo "  Expected json body :"$result
+                echo "Exiting....."
+                exit 1
+            fi
+        else
+            body="$(echo $body | tr -d '\n' )"
+            if [ "$RESULT" == "$body" ]; then
+                echo "  Expected body      :"$RESULT
+                echo "  Body as expected"
+            else
+                echo "  Expected body      :"$RESULT
+                echo "Exiting....."
+                exit 1
+            fi
+        fi
+    fi
+}