From 514637b303ec7dc6390007be1a046189f7a9d169 Mon Sep 17 00:00:00 2001 From: "halil.cakal" Date: Wed, 24 Aug 2022 14:50:57 +0100 Subject: [PATCH] Update version number in container-tag for F Maintenance Release Patch set 2: Cherry picks added and squashed. Issue-ID: NONRTRIC-757 Change-Id: I47b9b6c322ebdd8f6f29aa5f92c1b3847976efb9 Signed-off-by: halil.cakal --- .gitignore | 3 + LICENSE.txt | 201 +++++++++++++ docs/callout-server.rst | 255 ++++++++++++++++ docs/conf.py | 27 +- docs/images/yaml_logo.png | Bin 0 -> 3477 bytes docs/index.rst | 4 +- docs/kafka-message-dispatcher.rst | 220 ++++++++++++++ docs/release-notes.rst | 8 +- docs/requirements-docs.txt | 4 + near-rt-ric-simulator/README.md | 46 ++- near-rt-ric-simulator/api/STD_1.1.3/STD_A1.yaml | 2 +- near-rt-ric-simulator/container-tag.yaml | 2 +- near-rt-ric-simulator/src/STD_2.0.0/a1.py | 53 +++- near-rt-ric-simulator/test/EXT_SRV/.gitignore | 16 + near-rt-ric-simulator/test/EXT_SRV/README.md | 78 +++++ .../test/EXT_SRV/api/EXT_SRV_api.yaml | 9 +- .../test/EXT_SRV/docs/_static/logo.png | Bin 0 -> 43935 bytes near-rt-ric-simulator/test/EXT_SRV/docs/conf.py | 13 + near-rt-ric-simulator/test/EXT_SRV/docs/conf.yaml | 3 + .../test/EXT_SRV/docs/ext-srv-api.rst | 12 + .../test/EXT_SRV/docs/favicon.ico | Bin 0 -> 15086 bytes near-rt-ric-simulator/test/EXT_SRV/docs/index.rst | 24 ++ .../test/EXT_SRV/docs/overview.rst | 22 ++ .../test/EXT_SRV/docs/release-notes.rst | 49 ++++ near-rt-ric-simulator/test/EXT_SRV/tox.ini | 49 ++++ .../test/KAFKA_DISPATCHER/.gitignore | 16 + .../test/KAFKA_DISPATCHER/Dockerfile | 49 ++++ .../test/KAFKA_DISPATCHER/README.md | 78 +++++ .../KAFKA_DISPATCHER/api/KAFKA_DISPATCHER_api.yaml | 242 +++++++++++++++ .../test/KAFKA_DISPATCHER/certificate/cert.crt | 24 ++ .../certificate/generate_cert_and_key.sh | 26 ++ .../test/KAFKA_DISPATCHER/certificate/key.crt | 30 ++ .../test/KAFKA_DISPATCHER/certificate/pass | 1 + .../test/KAFKA_DISPATCHER/nginx.conf | 93 ++++++ .../resources/policytype_to_topicmap.json | 14 + .../test/KAFKA_DISPATCHER/src/dispatcher.py | 326 +++++++++++++++++++++ .../test/KAFKA_DISPATCHER/src/main.py | 77 +++++ .../test/KAFKA_DISPATCHER/src/maincommon.py | 123 ++++++++ .../test/KAFKA_DISPATCHER/src/payload_logging.py | 60 ++++ .../test/KAFKA_DISPATCHER/src/start.sh | 31 ++ .../test/KAFKA_DISPATCHER/src/var_declaration.py | 26 ++ .../test/KAFKA_DISPATCHER_TEST/basic_test.sh | 166 +++++++++++ .../test/KAFKA_DISPATCHER_TEST/build_and_start.sh | 53 ++++ .../jsonfiles/ANR_to_topic_map.json | 4 + .../jsonfiles/alpha_policy.json | 11 + .../jsonfiles/beta_policy.json | 11 + .../jsonfiles/forced_response.json | 5 + .../jsonfiles/timeout_response.json | 5 + .../test/KAFKA_DISPATCHER_TEST/timeout_test.sh | 139 +++++++++ .../test/STD_2.0.0/build_and_start_with_kafka.sh | 102 +++++++ .../test/common/consume_events_from_kafka_bus.py | 125 ++++++++ .../common/publish_response_event_to_kafka_bus.py | 88 ++++++ near-rt-ric-simulator/test/common/test_common.sh | 35 ++- tox.ini | 22 +- 54 files changed, 3047 insertions(+), 35 deletions(-) create mode 100755 LICENSE.txt create mode 100755 docs/callout-server.rst create mode 100644 docs/images/yaml_logo.png create mode 100755 docs/kafka-message-dispatcher.rst mode change 100644 => 100755 near-rt-ric-simulator/src/STD_2.0.0/a1.py create mode 100644 near-rt-ric-simulator/test/EXT_SRV/.gitignore create mode 100644 near-rt-ric-simulator/test/EXT_SRV/README.md create mode 100644 near-rt-ric-simulator/test/EXT_SRV/docs/_static/logo.png create mode 100755 near-rt-ric-simulator/test/EXT_SRV/docs/conf.py create mode 100755 near-rt-ric-simulator/test/EXT_SRV/docs/conf.yaml create mode 100644 near-rt-ric-simulator/test/EXT_SRV/docs/ext-srv-api.rst create mode 100644 near-rt-ric-simulator/test/EXT_SRV/docs/favicon.ico create mode 100644 near-rt-ric-simulator/test/EXT_SRV/docs/index.rst create mode 100644 near-rt-ric-simulator/test/EXT_SRV/docs/overview.rst create mode 100644 near-rt-ric-simulator/test/EXT_SRV/docs/release-notes.rst create mode 100644 near-rt-ric-simulator/test/EXT_SRV/tox.ini create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/.gitignore create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/Dockerfile create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/README.md create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/api/KAFKA_DISPATCHER_api.yaml create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/cert.crt create mode 100755 near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/generate_cert_and_key.sh create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/key.crt create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/pass create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/nginx.conf create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/resources/policytype_to_topicmap.json create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/dispatcher.py create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/main.py create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/maincommon.py create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/payload_logging.py create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/start.sh create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/var_declaration.py create mode 100755 near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/basic_test.sh create mode 100755 near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/build_and_start.sh create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/ANR_to_topic_map.json create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/alpha_policy.json create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/beta_policy.json create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/forced_response.json create mode 100644 near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/timeout_response.json create mode 100755 near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/timeout_test.sh create mode 100755 near-rt-ric-simulator/test/STD_2.0.0/build_and_start_with_kafka.sh create mode 100644 near-rt-ric-simulator/test/common/consume_events_from_kafka_bus.py create mode 100644 near-rt-ric-simulator/test/common/publish_response_event_to_kafka_bus.py diff --git a/.gitignore b/.gitignore index 50aeff4..00f2c95 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ docs/_build/ .coverage coverage.xml htmlcov/ + +# Python virtual env +venv/ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100755 index 0000000..96589bf --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/callout-server.rst b/docs/callout-server.rst new file mode 100755 index 0000000..f2556f4 --- /dev/null +++ b/docs/callout-server.rst @@ -0,0 +1,255 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2022 Nordix + +.. |nbsp| unicode:: 0xA0 + :trim: + +.. |nbh| unicode:: 0x2011 + :trim: + +.. |yaml-icon| image:: ./images/yaml_logo.png + :width: 40px + +.. _calloutserver: + +===================== +Callout Server +===================== + +API Documentation +================= + +The O-RAN SC external call-out server allows behavioral extensions to be added to the A1 Simulator. It creates an external call-out server, which provides a RESTful API. A1 Policy operations, for some A1 Policy Types, can then be redirected to the external call-out server, supporting supplemental simulator behavior for those A1 Policy Types. + +**Note:** call-out server functionality is only available for *'STD_2.0.0'* version simulators. + +The external call-out server exposes a 'Callout server API' REST API. This internal API is invoked directly by the A1 Simulator, and is not intended to be used by any other client. The 'Callout Server API' is documented in `Callout Server API <./EXT_SRV_api.html>`_ and in OpenAPI YAML format: + +.. csv-table:: + :header: "API name", "|yaml-icon|" + :widths: 10,5 + + "Callout Server API", ":download:`link <../near-rt-ric-simulator/test/EXT_SRV/api/EXT_SRV_api.yaml>`" + +External call-out servers also expose an 'Admin API' to manipulate the behavior of the call-out server itself. The 'Callout Server Admin API' is documented below: + +Admin Functions +================ + +Health Check +------------ + +GET ++++ + +Returns the status of the external server. + +**URL path:** + / + +**Parameters:** + None. + +**Responses:** + 200: + OK + +**Examples:** + +**Call**: :: + + curl -X GET "http://localhost:9095/" + +**Result**: + +200: :: + + OK + + +Delete all policy instances in external server +---------------------------------------------- + +POST +++++ + +Delete all policy instances. + +**URL path:** + +/serveradmin/deleteinstances + +**Parameters:** + +None. + +**Responses:** + +200: + +All a1 policy instances deleted + +**Examples:** + +**Call**: :: + + curl -X POST "http://localhost:9095/serveradmin/deleteinstances" + +**Result**: + +200: :: + + All a1 policy instances deleted + + +Response manipulation +--------------------- +It is possible to manipulate the response of all operations on the external server + +POST +++++ + +Force a specific response code for the all (the next) external server operation. Unless it is reset, it will always respond the same response code back. + +**URL path:** + +/serveradmin/forceresponse?code= + +**Parameters:** + +code: (*Required*) + +The HTTP response code to return. + +**Responses:** + +200: + +Force response code: set for all external server response until it is resetted + +**Examples:** + +**Call**: :: + + curl -X POST "http://localhost:9095/serveradmin/forceresponse?code=500" + +**Result**: + +200: :: + + Force response code: 500 set for all external server response until it is resetted + + +Reset response-manipulation +--------------------------- +It is possible to reset the response manipulation on the external server + +POST +++++ + +Clears specific response code for all (the next) external server operation. + +**URL path:** + +/serveradmin/forceresponse?code= + +**Parameters:** + +code: (*Required*) + +The HTTP response code to return. + +**Responses:** + +200: + +Force response code has been resetted for all external server responses + +**Examples:** + +**Call**: :: + + curl -X POST "http://localhost:9095/serveradmin/forceresponse?code=500" + +**Result**: + +200: :: + + Force response code has been resetted for all external server responses + + +Response time manipulation +-------------------------- +It is possible to set a period of time to delay response time. + +POST +++++ + +Force delayed response of all A1 responses. The setting will remain until the delay is set to '0' + +**URL path:** + +/serveradmin/forcedelay?delay= + +**Parameters:** + +delay: (*Required*) + +The time in seconds to delay all responses. + +**Responses:** + +200: + +Force delay: sec set for all external server responses until it is resetted + +**Examples:** + +**Call**: :: + + curl -X POST "http://localhost:9095/serveradmin/forcedelay?delay=5" + +**Result**: + +200: :: + + Force delay: 5 sec set for all external server responses until it is resetted + + +Reset response time manipulation +-------------------------------- +It is also possible to reset delay response time. + +POST +++++ + +The setting will clear the delay. + +**URL path:** + +/serveradmin/forcedelay + +**Parameters:** + +None. + +The time in seconds to delay all responses. + +**Responses:** + +200: + +Force delay has been resetted for all external server responses + +**Examples:** + +**Call**: :: + + curl -X POST "http://localhost:9095/serveradmin/forcedelay" + +**Result**: + +200: :: + + Force delay has been resetted for all external server responses diff --git a/docs/conf.py b/docs/conf.py index 07032ac..42330a6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,32 @@ from docs_conf.conf import * +branch = 'latest' + +language = 'en' + linkcheck_ignore = [ 'http://localhost.*', 'http://127.0.0.1.*', - 'https://gerrit.o-ran-sc.org.*' + 'https://gerrit.o-ran-sc.org.*', + './KAFKA_DISPATCHER_api.html', + './EXT_SRV_api.html', #Generated file that doesn't exist at link check. ] + +extensions = ['sphinxcontrib.redoc'] + +redoc = [ + { + 'name': 'CALLOUT SERVER', + 'page': 'EXT_SRV_api', + 'spec': '../near-rt-ric-simulator/test/EXT_SRV/api/EXT_SRV_api.yaml', + 'embed': True, + }, + { + 'name': 'Kafka Message Dispatcher', + 'page': 'KAFKA_DISPATCHER_api', + 'spec': '../near-rt-ric-simulator/test/KAFKA_DISPATCHER/api/KAFKA_DISPATCHER_api.yaml', + 'embed': True, + }, + ] + +redoc_uri = 'https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js' diff --git a/docs/images/yaml_logo.png b/docs/images/yaml_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0492eb4bac42eda52e071cd68ac9c23cb606f72c GIT binary patch literal 3477 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 literal 0 HcmV?d00001 diff --git a/docs/index.rst b/docs/index.rst index 95a0997..b96a895 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,6 @@ .. This work is licensed under a Creative Commons Attribution 4.0 International License. .. SPDX-License-Identifier: CC-BY-4.0 -.. Copyright (C) 2021 Nordix +.. Copyright (C) 2021-2022 Nordix .. |nbsp| unicode:: 0xA0 :trim: @@ -20,5 +20,7 @@ A1 Interface Simulator ./overview.rst ./simulator-api.rst + ./callout-server.rst + ./kafka-message-dispatcher.rst ./release-notes.rst diff --git a/docs/kafka-message-dispatcher.rst b/docs/kafka-message-dispatcher.rst new file mode 100755 index 0000000..9bfebf7 --- /dev/null +++ b/docs/kafka-message-dispatcher.rst @@ -0,0 +1,220 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2022 Nordix + +.. |nbsp| unicode:: 0xA0 + :trim: + +.. |nbh| unicode:: 0x2011 + :trim: + +.. |yaml-icon| image:: ./images/yaml_logo.png + :width: 40px + +======================== +Kafka Message Dispatcher +======================== + +API Documentation +================= + +The O-RAN SC Kafka Message Dispatcher is a specific implementation of an A1 Simulator ref:`calloutserver`, which further redirects A1 Policy operations to a Kafka message topic, to be consumed by an external function. + +A1 Policy are redirected as Kafka messages to a configured Kafka Topic to an external receiver, then responses from the external receiver are collected from another configured Kafka Topic. This provides a Kafka-based request-response abstraction for adding supplemental simulator behavior for particular A1 Policy Types. After a request message is sent, a response message will be expected within some configurable timeout interval (default: 30 sec). The topics to be used for particular A1 Policy Types is configured using a JSON map (Example: `policytype_to_topicmap.json <../near-rt-ric-simulator/test/KAFKA_DISPATCHER/resources/policytype_to_topicmap.json>` + +**Note:** As with other A1 Simulator call-out servers, the Kafka message dispatcher functionality is only available for *'STD_2.0.0'* version simulators. + +The Kafka message dispatcher exposes a 'Kafka Message Dispatcher' REST API. This internal API is invoked directly by the A1 Simulator, and is not intended to be used by any other client. This API is documented in `Kafka Message Dispatcher API <./KAFKA_DISPATCHER_api.html>`_ and in OpenAPI YAML format: + +.. csv-table:: + :header: "API name", "|yaml-icon|" + :widths: 10,5 + + "Kafka Message Dispatcher API", ":download:`link <../near-rt-ric-simulator/test/KAFKA_DISPATCHER/api/KAFKA_DISPATCHER_api.yaml>`" + +The Kafka message dispatcher also exposes an 'Admin API' to manipulate the behavior of the Kafka message dispather itself. The 'Kafka Message Dispatcher Admin API' is documented below: + +Admin Functions +================ + +Health Check +------------ + +GET ++++ + +Returns the status of the Kafka Message Dispatcher. + +**URL path:** + / + +**Parameters:** + None. + +**Responses:** + 200: + OK + +**Examples:** + +**Call**: :: + + curl -X GET "http://localhost:7075/" + +**Result**: + +200: :: + + OK + + +Response manipulation +--------------------- +It is possible to manipulate the response of all operations on the Kafka Message Dispatcher module + +POST +++++ + +Force a specific response code for the all (the next) Kafka Message Dispatcher module operations. Unless it is reset, it will always respond the same response code back. + +**URL path:** + +/dispatcheradmin/forceresponse?code= + +**Parameters:** + +code: (*Required*) + +The HTTP response code to return. + +**Responses:** + +200: + +Force response code: set for all all dispatcher response until it is resetted + +**Examples:** + +**Call**: :: + + curl -X POST "http://localhost:7075/dispatcheradmin/forceresponse?code=500" + +**Result**: + +200: :: + + Force response code: 500 set for all dispatcher response until it is resetted + + +Reset response-manipulation +--------------------------- +It is possible to reset the response manipulation on the Kafka Message Dispatcher module + +POST +++++ + +Clears specific response code for all (the next) Kafka Message Dispatcher module operation. + +**URL path:** + +/dispatcheradmin/forceresponse + +**Parameters:** + +code: (*Required*) + +The HTTP response code to return. + +**Responses:** + +200: + +Force response code has been resetted for dispatcher responses + +**Examples:** + +**Call**: :: + + curl -X POST "http://localhost:7075/dispatcheradmin/forceresponse" + +**Result**: + +200: :: + + Force response code has been resetted for dispatcher responses + + +Response time manipulation +-------------------------- +It is possible to set a period of time to delay response time. + +POST +++++ + +Force delayed response of all dispatcher responses. The setting will remain until the delay is cleared. + +**URL path:** + +/dispatcheradmin/forcedelay?delay= + +**Parameters:** + +delay: (*Required*) + +The time in seconds to delay all responses. + +**Responses:** + +200: + +Force delay: sec set for all dispatcher responses until it is resetted + +**Examples:** + +**Call**: :: + + curl -X POST "http://localhost:7075/dispatcheradmin/forcedelay?delay=5" + +**Result**: + +200: :: + + Force delay: 5 sec set for all dispatcher responses until it is resetted + + +Reset response time manipulation +-------------------------------- +It is also possible to reset delay response time. + +POST +++++ + +The setting will clear the delay. + +**URL path:** + +/dispatcheradmin/forcedelay + +**Parameters:** + +None. + +The time in seconds to delay all responses. + +**Responses:** + +200: + +Force delay has been resetted for all dispatcher responses + +**Examples:** + +**Call**: :: + + curl -X POST "http://localhost:7075/dispatcheradmin/forcedelay" + +**Result**: + +200: :: + + Force delay has been resetted for all dispatcher responses diff --git a/docs/release-notes.rst b/docs/release-notes.rst index f2e7251..9f605b0 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -1,21 +1,21 @@ .. This work is licensed under a Creative Commons Attribution 4.0 International License. .. http://creativecommons.org/licenses/by/4.0 -.. Copyright (C) 2021 Nordix +.. Copyright (C) 2021-2022 Nordix ============= Release-Notes ============= -This document provides the release notes for the release of the Near-RT RIC A1 Interface Simulator. +This document provides the release notes for the release of the A1 Simulator (previously called Near-RT RIC A1 Interface). .. contents:: :depth: 3 :local: -Version history Near-RT RIC A1 Interface Simulator -================================================== +Version history A1 Simulator (previously called Near-RT RIC A1 Interface) +========================================================================= +------------+----------+------------------+----------------+ | **Date** | **Ver.** | **Author** | **Comment** | diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 09a0c1c..4cfc69f 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,9 @@ sphinx sphinx-rtd-theme +sphinx-bootstrap-theme sphinxcontrib-httpdomain +sphinxcontrib-redoc +sphinxcontrib-needs +sphinxcontrib-swaggerdoc recommonmark lfdocs-conf diff --git a/near-rt-ric-simulator/README.md b/near-rt-ric-simulator/README.md index 7ae28f0..235d592 100644 --- a/near-rt-ric-simulator/README.md +++ b/near-rt-ric-simulator/README.md @@ -1,6 +1,6 @@ -# O-RAN-SC Near-RealTime RIC Simulator +# O-RAN-SC A1 Simulator -The O-RAN SC Near-RealTime RIC simulates the A1 as an generic REST API which can receive and send northbound messages. The simulator validates the payload and applies policy. +The O-RAN SC A1 simulator simulates the A1 as an generic REST API which can receive and send northbound messages. The simulator validates the payload and applies policy. The simulator supports multiple A1 interface versions (version of the open API yaml file\): @@ -222,15 +222,49 @@ Go to the test folder of the selected version, 'test/<version>/. Note that test can be performed both using the nonsecure http port and the secure https port. -Build and start the simulator container using: +Build and start the simulator containers: STD_1.1.3 and OSC_2.1.0, using: -./build\_and\_start.sh duplicate-check|ignore-duplicate +./build_and_start.sh duplicate-check|ignore-duplicate +Build and start the simulator container version STD_2.0.0, using two alternatives: ext-srv or kafka-srv. However, both can not be used at the same time to start A1 sim. + +In order to start with ext-srv: +./build_and_start.sh duplicate-check|ignore-duplicate ext-srv|ext-srv-secure|ignore-ext-srv + +In order to start with kafka-srv: +./build_and_start.sh duplicate-check|ignore-duplicate kafka-srv|kafka-srv-secure publish-resp|ignore-publish + +STD_2.0.0 version is now including an external server that is a Python server building RESTful API. The external server supports HTTP/HTTPS protocols. +The description of the start parameters are explained below: +ext-srv: Runs external server that supports HTTP protocol only. +ext-srv-secure: Runs external server that supports HTTPS protocol as well. +ignore-ext-srv: Ignores external server to run. + +STD_2.0.0 version also includes an kafka message dispatcher that is a Python server building RESTful APIs. The kafka server supports HTTP/HTTPS protocols. +The description of the start parameters are explained below: +kafka-srv: Runs kafka server that supports HTTP protocol only. +kafka-srv-secure: Runs kafka server that supports HTTPS protocol as well. +publish-resp: The flag controls the dispatcher module to decide auto responding to each requests for test purposes only. +ignore-publish: If the A1 sim is being started using ignore flag, then the dispatcher module will look for a respone message published by south-bound module. This will build and start the container in interactive mode. The built container only resides in the local docker repository. Note, the default port is 8085 for http and 8185 for https. When running the simulator as a container, the defualt ports can be re-mapped to any port on the localhost. -In a second terminal, go to the same folder and run the basic test script, basic\_test.sh nonsecure|secure or commands.sh nonsecure|secure duplicate-check|ignore-duplicate +In a second terminal, go to the same folder and run the basic test script, basic_test.sh nonsecure|secure or commands.sh nonsecure|secure duplicate-check|ignore-duplicate for STD_1.1.3 and OSC_2.1.0 versions. + +For the STD_2.0.0 version, in a second terminal, go to the same folder and run the basic test script for external server activated case: +./basic_test.sh nonsecure|secure duplicate-check|ignore-duplicate ext-srv|ext-srv-secure|ignore-ext-srv +The description of the test script parameters are explained below: +nonsecure|secure: Runs test cases with either support of HTTP/HTTPS protocol. +duplicate-check|ignore-duplicate: Runs test cases with either support of duplicate/ignore-duplicate flag for the policies. +ext-srv|ext-srv-secure|ignore-ext-srv: If the simulator started with ext-srv or ext-srv-secure parameter, then one of these options can be used. Otherwise, ignore-ext-srv parameter should be used. + +For the STD_2.0.0 version, in a second terminal, go to the same folder and run the basic test script for kafka dispatcher server activated case: +./basic_test.sh nonsecure|secure duplicate-check|ignore-duplicate ext-srv|ext-srv-secure|ignore-ext-srv +The description of the test script parameters are explained below: +nonsecure|secure: Runs test cases with either support of HTTP/HTTPS protocol. +duplicate-check|ignore-duplicate: Runs test cases with either support of duplicate/ignore-duplicate flag in accordance with the one which used while starting A1 sim. +ext-srv|ext-srv-secure|ignore-ext-srv: If the simulator started with kafka-srv or kafka-srv-secure parameter, then ignore-ext-srv option should be used. Note that the arg for duplicate check must match in both scripts. This script runs a number of tests towards the simulator to make sure it works properply. @@ -242,7 +276,7 @@ Only http is tested as the internal flask server is only using http (https is pa Navigate to 'near-rt-ric-simulator/tests'. Choose the version to test and use that file for test. -Use 'python3 -m pytest \' to run unit test only with no coverage check +Use 'python3 -m pytest \' to run unit test only with no coverage check. Before running that command, the dependencies which are pytest and connexion should be installed in your virtual environment. If the latest connexion version arises DeprecationWarning, you may try to install connexion with version 2.6.0. Or use 'coverage run -m pytest \' to run unit test and produce coverage data. diff --git a/near-rt-ric-simulator/api/STD_1.1.3/STD_A1.yaml b/near-rt-ric-simulator/api/STD_1.1.3/STD_A1.yaml index 71638f4..002bde4 100644 --- a/near-rt-ric-simulator/api/STD_1.1.3/STD_A1.yaml +++ b/near-rt-ric-simulator/api/STD_1.1.3/STD_A1.yaml @@ -255,4 +255,4 @@ components: content: application/problem+json: schema: - "$ref": "#/components/schemas/ProblemDetails" \ No newline at end of file + "$ref": "#/components/schemas/ProblemDetails" diff --git a/near-rt-ric-simulator/container-tag.yaml b/near-rt-ric-simulator/container-tag.yaml index dc2a86d..ea5c2b0 100644 --- a/near-rt-ric-simulator/container-tag.yaml +++ b/near-rt-ric-simulator/container-tag.yaml @@ -1,2 +1,2 @@ --- -tag: 2.3.0 +tag: 2.3.1 diff --git a/near-rt-ric-simulator/src/STD_2.0.0/a1.py b/near-rt-ric-simulator/src/STD_2.0.0/a1.py old mode 100644 new mode 100755 index 28eccc6..e70a8ed --- a/near-rt-ric-simulator/src/STD_2.0.0/a1.py +++ b/near-rt-ric-simulator/src/STD_2.0.0/a1.py @@ -36,7 +36,7 @@ APPL_JSON='application/json' APPL_PROB_JSON='application/problem+json' EXT_SRV_URL=os.getenv('EXT_SRV_URL') - +KAFKA_DISPATCHER_URL=os.getenv('KAFKA_DISPATCHER_URL') # API Function: Get all policy type ids def get_all_policy_types(): @@ -132,6 +132,13 @@ def put_policy(policyTypeId, policyId): pjson=create_problem_json(None, "Duplicate, the policy json already exists.", 400, None, policy_id) return Response(json.dumps(pjson), 400, mimetype=APPL_PROB_JSON) + #Callout hooks for kafka dispatcher + if (KAFKA_DISPATCHER_URL is not None): + resp = callout_kafka_dispatcher(policy_type_id, policy_id, data, retcode) + if (resp != 200): + pjson=create_error_response(resp) + return Response(json.dumps(pjson), 500, mimetype=APPL_PROB_JSON) + #Callout hooks for external server #When it fails, break and return 419 HTTP status code if (EXT_SRV_URL is not None): @@ -203,6 +210,13 @@ def delete_policy(policyTypeId, policyId): pjson=create_problem_json(None, "The requested policy does not exist.", 404, None, policy_id) return Response(json.dumps(pjson), 404, mimetype=APPL_PROB_JSON) + #Callout hooks for kafka dispatcher + if (KAFKA_DISPATCHER_URL is not None): + resp = callout_kafka_dispatcher(policy_type_id, policy_id, None, 204) + if (resp != 200): + pjson=create_error_response(resp) + return Response(json.dumps(pjson), 500, mimetype=APPL_PROB_JSON) + #Callout hooks for external server #When it fails, break and return 419 HTTP status code if (EXT_SRV_URL is not None): @@ -241,8 +255,45 @@ def get_policy_status(policyTypeId, policyId): pjson=create_problem_json(None, "The requested policy does not exist.", 404, None, policy_id) return Response(json.dumps(pjson), 404, mimetype=APPL_PROB_JSON) + #Callout hooks for kafka dispatcher + if (KAFKA_DISPATCHER_URL is not None): + resp = callout_kafka_dispatcher(policy_type_id, policy_id, None, 202) + if (resp != 200): + pjson=create_error_response(resp) + return Response(json.dumps(pjson), 500, mimetype=APPL_PROB_JSON) + return Response(json.dumps(policy_status[policy_id]), status=200, mimetype=APPL_JSON) + +# Helper: Callout kafka dispatcher server to notify it for policy operations +def callout_kafka_dispatcher(policy_type_id, policy_id, payload, retcode): + + target_url = KAFKA_DISPATCHER_URL + "/policytypes/" + policy_type_id + "/kafkadispatcher/" + policy_id + try: + # create operation, publish with payload + if (retcode == 201): + resp=requests.put(target_url, json=payload, timeout=30, verify=False) + return resp.status_code + # update operation, publish with payload + elif (retcode == 200): + # add headers an update-flag + headers = {'updateoper' : 'yes'} + resp=requests.put(target_url, json=payload, headers=headers, timeout=30, verify=False) + return resp.status_code + # delete operation, publish without payload + elif (retcode == 204): + resp=requests.delete(target_url, timeout=30, verify=False) + return resp.status_code + # get policy status operation, publish without payload + elif (retcode == 202): + # update endpoint + target_url = target_url + "/status" + resp=requests.get(target_url, timeout=30, verify=False) + return resp.status_code + except Exception: + return 419 + + # Helper: Callout external server to notify it for policy operations # Returns 200, 201 and 204 for the success callout hooks, for the others returns 419 def callout_external_server(policy_id, payload, operation): diff --git a/near-rt-ric-simulator/test/EXT_SRV/.gitignore b/near-rt-ric-simulator/test/EXT_SRV/.gitignore new file mode 100644 index 0000000..00f2c95 --- /dev/null +++ b/near-rt-ric-simulator/test/EXT_SRV/.gitignore @@ -0,0 +1,16 @@ +# Documentation +.idea/ +.tox +docs/_build/ +.DS_STORE + +# IDE +.project +.vscode + +.coverage +coverage.xml +htmlcov/ + +# Python virtual env +venv/ diff --git a/near-rt-ric-simulator/test/EXT_SRV/README.md b/near-rt-ric-simulator/test/EXT_SRV/README.md new file mode 100644 index 0000000..f1c1f57 --- /dev/null +++ b/near-rt-ric-simulator/test/EXT_SRV/README.md @@ -0,0 +1,78 @@ +# O-RAN-SC External Server extension for A1 Simulator + +The O-RAN SC external server is an extension for A1 simulator. It creates an external web server building RESTful API. It is capable of recieving Rest calls from the northbound simulator version and responses back to it. + +The external server supports GET, PUT and DELETE operations (version of the open API yaml file\): + +| Yaml file version | Version id | +| --------------------- | ------------------- | +| EXT_SRV_api.yaml | 0.0.1 | + +The overall folder structure is \(relative to the location of this README file\): + +| Dir | Description | +| ---------------- | ----------- | +|. |Dockerfile, tox.ini and README | +|api |The open api yaml | +|src |Python source code | +|certificate |A self-signed certificate and a key | +|docs |Auto generated API descriptions in HTML format | + +The external server handles the requests that are defined in the open API yaml file. All these requests are implemented in the server.py file in the src folder. In addition, a number of administrative functions are also supported and implemented by the main.py in the source folder. + +The section below outlines the supported open api rest-based operations as well as the adminstrative operations. + +# Ports and certificates + +The external server normally opens the port 9095 for http. If a certificate and a key are provided the external server will open port 9195 for https instead. The port 9195 is only opened if a valid certificate and key is found. +The certificate and key shall be placed in the same directory and the directory shall be mounted to /usr/src/app/cert in the container. + +| Port | Protocol | +| -------- | ----- | +| 9095 | http | +| 9195 | https | + +The directory certificate contains a self-signed cert. Use the script generate_cert_and_key.sh to generate a new certificate and key. The password of the certificate must be set 'test'. +The same urls are availables on both the http port 9095 and the https port 9195. If using curl and https, the flag -k shall be given to make curl ignore checking the certificate. + +# Supported operations in External Server 0.0.1 + + +For the complete yaml specification, see [openapi.yaml](../api/EXT_SRV_api.yaml) + +URIs for server: + +| Function | Path and parameters | +| --------------------- | ------------------- | +| GET, Get all A1 policy ids | localhost:9095/a1policies | +| GET, Query for an A1 policy | localhost:9095/a1policy/{a1policyId} | +| PUT, Create an A1 policy | localhost:9095/a1policy/{a1policyId} | +| DELETE, Delete an A1 policy | localhost:9095/a1policy/{a1policyId} | + +URIs for admin operations: + +| Function | Path and parameters | +| --------------------- | ------------------- | +| POST, Delete all A1 policy instances | localhost:9095/serveradmin/deleteinstances | +| POST, Force a specific response code for all A1 operation | localhost:9095/serveradmin/forceresponse?code=500 | +| POST, Reset force response code | localhost:9095/serveradmin/forceresponse | +| POST, Force delayed response of all A1 operations | localhost:9095/serveradmin/forcedelay?delay=5 | +| POST, Reset force delayed response | localhost:9095/serveradmin/forcedelay | + + +## License + +Copyright (C) 2022 Nordix Foundation. +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. + +For more information about license please see the [LICENSE](LICENSE.txt) file for details. 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 index d37db61..4765e81 100644 --- 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 @@ -1,19 +1,22 @@ openapi: 3.0.0 info: title: 'External Server for A1 simulator' - version: 2.0.0 + version: 0.0.1 description: | External test server. © 2022, O-RAN Alliance. All rights reserved. + license: + name: Copyright (C) 2022 Nordix Foundation. Licensed under the Apache License. + url: http://www.apache.org/licenses/LICENSE-2.0 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' + url: 'https://docs.o-ran-sc.org/projects/o-ran-sc-sim-a1-interface/en/latest/EXT_SRV_api.html' servers: - url: '{apiRoot}' variables: apiRoot: - default: 'https://testserver.com' + default: 'http://www.example.com' paths: '/a1policies': get: diff --git a/near-rt-ric-simulator/test/EXT_SRV/docs/_static/logo.png b/near-rt-ric-simulator/test/EXT_SRV/docs/_static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c3b6ce56468d87a3d9463ee75297b3895fc9a414 GIT binary patch literal 43935 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

r@f1gRWk|A*%4`3la7*fhEBO++ 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 literal 0 HcmV?d00001 diff --git a/near-rt-ric-simulator/test/EXT_SRV/docs/conf.py b/near-rt-ric-simulator/test/EXT_SRV/docs/conf.py new file mode 100755 index 0000000..011a69d --- /dev/null +++ b/near-rt-ric-simulator/test/EXT_SRV/docs/conf.py @@ -0,0 +1,13 @@ +from docs_conf.conf import * + +language = 'en' + +extensions = [ + 'sphinxcontrib.openapi', +] + +linkcheck_ignore = [ + 'http://localhost.*', + 'http://127.0.0.1.*', + 'https://gerrit.o-ran-sc.org.*' +] diff --git a/near-rt-ric-simulator/test/EXT_SRV/docs/conf.yaml b/near-rt-ric-simulator/test/EXT_SRV/docs/conf.yaml new file mode 100755 index 0000000..6576ed2 --- /dev/null +++ b/near-rt-ric-simulator/test/EXT_SRV/docs/conf.yaml @@ -0,0 +1,3 @@ +--- +project_cfg: oran +project: sim-a1-interface diff --git a/near-rt-ric-simulator/test/EXT_SRV/docs/ext-srv-api.rst b/near-rt-ric-simulator/test/EXT_SRV/docs/ext-srv-api.rst new file mode 100644 index 0000000..d2be12f --- /dev/null +++ b/near-rt-ric-simulator/test/EXT_SRV/docs/ext-srv-api.rst @@ -0,0 +1,12 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2022 Nordix + +.. _ext-srv-api: + +========================== +External Server API +========================== + +.. Generates content from EXT_SRV_api.yaml +.. openapi:: ../api/EXT_SRV_api.yaml diff --git a/near-rt-ric-simulator/test/EXT_SRV/docs/favicon.ico b/near-rt-ric-simulator/test/EXT_SRV/docs/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..00b0fd0ef0b4e78fbb8cdb413ce84561dfeb404f GIT binary patch literal 15086 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

4K$ruCE1 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 XY+}&}KC=mD;X?Iq*l*cA?B4$ePY*3q literal 0 HcmV?d00001 diff --git a/near-rt-ric-simulator/test/EXT_SRV/docs/index.rst b/near-rt-ric-simulator/test/EXT_SRV/docs/index.rst new file mode 100644 index 0000000..db26e4c --- /dev/null +++ b/near-rt-ric-simulator/test/EXT_SRV/docs/index.rst @@ -0,0 +1,24 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2022 Nordix + +.. |nbsp| unicode:: 0xA0 + :trim: + +.. |nbh| unicode:: 0x2011 + :trim: + +.. _a1-interface-ext-srv: + +====================== +A1 Interface EXT SRV +====================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + ./overview.rst + ./ext-srv-api.rst + ./release-notes.rst + diff --git a/near-rt-ric-simulator/test/EXT_SRV/docs/overview.rst b/near-rt-ric-simulator/test/EXT_SRV/docs/overview.rst new file mode 100644 index 0000000..8da86d2 --- /dev/null +++ b/near-rt-ric-simulator/test/EXT_SRV/docs/overview.rst @@ -0,0 +1,22 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2020 Nordix + +.. |nbsp| unicode:: 0xA0 + :trim: + +.. |nbh| unicode:: 0x2011 + :trim: + + +A1 Simulator EXT SRV +===================== + +The A1 Simulator terminates the A1 interface and provides a way to test Non-RT RIC services without the need to deploy Near |nbh| RT |nbsp| RICs. + +Apart from providing the A1 API, the simulator also provides an administrative API to manage policy types and manipulate +the simulator, see ":ref:`ext-srv-api`". + +The A1 Simulator supports running multiple simulations using different versions of the A1 Application protocol, and supports realistic stateful simulation of A1 Enrichment Information and A1 Policy behaviours. + +For information on how to run the simulator, see the *README.md* file in the repository. diff --git a/near-rt-ric-simulator/test/EXT_SRV/docs/release-notes.rst b/near-rt-ric-simulator/test/EXT_SRV/docs/release-notes.rst new file mode 100644 index 0000000..d97e95c --- /dev/null +++ b/near-rt-ric-simulator/test/EXT_SRV/docs/release-notes.rst @@ -0,0 +1,49 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. http://creativecommons.org/licenses/by/4.0 +.. Copyright (C) 2022 Nordix + +============= +Release-Notes +============= + + +This document provides the release notes for the release of the Near-RT RIC A1 Interface Simulator. + +.. contents:: + :depth: 3 + :local: + + +Version history Near-RT RIC A1 Interface Simulator +================================================== + ++------------+----------+------------------+----------------+ +| **Date** | **Ver.** | **Author** | **Comment** | +| | | | | ++------------+----------+------------------+----------------+ +| 2022-06-29 | 2.3.0 | Halil Cakal | F Release | +| | | | | ++------------+----------+------------------+----------------+ + +Release Data +============ + +F Release +--------- ++-----------------------------+-------------------------------------------------------+ +| **Project** | Non-RT RIC | +| | | ++-----------------------------+-------------------------------------------------------+ +| **Repo/commit-ID** | a1-interface/595506e290356d26b8eebfab32ef8d3f625cbb0a | +| | | ++-----------------------------+-------------------------------------------------------+ +| **Release designation** | F | +| | | ++-----------------------------+-------------------------------------------------------+ +| **Release date** | 2022-06-29 | +| | | ++-----------------------------+-------------------------------------------------------+ +| **Purpose of the delivery** | Added Callout hooks towards external server for | +| | create and delete operations | +| | | ++-----------------------------+-------------------------------------------------------+ diff --git a/near-rt-ric-simulator/test/EXT_SRV/tox.ini b/near-rt-ric-simulator/test/EXT_SRV/tox.ini new file mode 100644 index 0000000..082c408 --- /dev/null +++ b/near-rt-ric-simulator/test/EXT_SRV/tox.ini @@ -0,0 +1,49 @@ +# ================================================================================== +# Copyright (c) 2022 Nordix +# +# 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 = docs,docs-linkcheck +minversion = 2.0 +skipsdist = true + +# doc jobs +[testenv:docs] +whitelist_externals = echo +# Version 3.8 is required, otherwise AttributeError will arise +basepython = python3.8 +deps = + sphinx + sphinx-rtd-theme + sphinxcontrib-httpdomain + # Version 0.6.0 is required, otherwise BlockGrammer error will arise + sphinxcontrib-openapi==0.6.0 + recommonmark + lfdocs-conf +commands = + sphinx-build -W -b html -n -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/html + echo "Generated docs available in {toxinidir}/docs/_build/html" + +[testenv:docs-linkcheck] +skipsdist = true +basepython = python3.8 +deps = sphinx + sphinx-rtd-theme + sphinxcontrib-httpdomain + # Version 0.6.0 is required, otherwise BlockGrammer error will arise + sphinxcontrib-openapi==0.6.0 + recommonmark + lfdocs-conf +commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/linkcheck diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER/.gitignore b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/.gitignore new file mode 100644 index 0000000..00f2c95 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/.gitignore @@ -0,0 +1,16 @@ +# Documentation +.idea/ +.tox +docs/_build/ +.DS_STORE + +# IDE +.project +.vscode + +.coverage +coverage.xml +htmlcov/ + +# Python virtual env +venv/ diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER/Dockerfile b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/Dockerfile new file mode 100644 index 0000000..bc5d815 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/Dockerfile @@ -0,0 +1,49 @@ +# ============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] +RUN pip install kafka-python + +#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 +COPY resources resources + +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/KAFKA_DISPATCHER/README.md b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/README.md new file mode 100644 index 0000000..8b14200 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/README.md @@ -0,0 +1,78 @@ +# O-RAN-SC Kafka Dispatcher Module extension for A1 Simulator + +The O-RAN SC dispatcher module is an extension for A1 simulator. It creates a web server building RESTful APIs. It is capable of recieving Rest calls from the northbound simulator version and respond back to it. + +The kafka dispatcher module supports GET, PUT and DELETE operations (version of the open API yaml file\): + +| Yaml file version | Version id | +| -------------------------- |-------------------- | +| KAFKA_DISPATCHER_api.yaml | 0.0.1 | + +The overall folder structure is \(relative to the location of this README file\): + +| Dir | Description | +| ---------------- | ----------- | +|. |Dockerfile, tox.ini and README | +|api |The open api yaml | +|src |Python source code | +|certificate |A self-signed certificate and a key | +|docs |Auto generated API descriptions in HTML format | + +The dispatcher module handles the requests that are defined in the open API yaml file. All these requests are implemented in the dispatcher.py file in the src folder. In addition, a number of administrative functions are also supported and implemented by the main.py in the source folder. + +The section below outlines the supported open api rest-based operations as well as the adminstrative operations. + +# Ports and certificates + +The dispatcher module normally opens the port 7075 for http. If a certificate and a key are provided the kafka dispatcher module will open port 7175 for https instead. The port 7175 is only opened if a valid certificate and key is found. +The certificate and key shall be placed in the same directory and the directory shall be mounted to /usr/src/app/cert in the container. + +| Port | Protocol | +| -------- | ----- | +| 7075 | http | +| 7175 | https | + +The directory certificate contains a self-signed cert. Use the script generate_cert_and_key.sh to generate a new certificate and key. The password of the certificate must be set 'test'. +The same urls are availables on both the http port 7075 and the https port 7175. If using curl and https, the flag -k shall be given to make curl ignore checking the certificate. + +# Supported operations in Kafka Dispatcher Module 0.0.1 + + +For the complete yaml specification, see [openapi.yaml](../api/KAFKA_DISPATCHER_api.yaml) + +URIs for server: + +| Function | Path and parameters | +| --------------------- | ------------------- | +| GET, Get the kafka request and response topic map corresponding to policy type | localhost:7075/policytypetotopicmapping/ANR | +| GET, Dispatch policy status query opertion as kafka message to kafka cluster | localhost:7075/policytypes/ANR/kafkadispatcher/alpha/status | +| PUT, Dispatch create and update operation as kafka message to kafka cluster | localhost:7075/policytypes/ANR/kafkadispatcher/alpha | +| DELETE, Dispatch policy delete opertion as kafka message to kafka cluster | localhost:7075/policytypes/ptype1/kafkadispatcher/alpha | + +URIs for admin operations: + +| Function | Path and parameters | +| --------------------- | ------------------- | +| GET, Get status of dispatcher module | http://localhost:7075/ | +| POST, Force a specific response code for all dispatcher module operations | localhost:7075/dispatcheradmin/forceresponse?code=500 | +| POST, Reset force response code | localhost:7075/dispatcheradmin/forceresponse | +| POST, Force delayed response of all dispatcher module operations | localhost:7075/dispatcheradmin/forcedelay?delay=5 | +| POST, Reset force delayed response | localhost:7075/dispatcheradmin/forcedelay | + + +## License + +Copyright (C) 2022 Nordix Foundation. +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. + +For more information about license please see the [LICENSE](LICENSE.txt) file for details. diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER/api/KAFKA_DISPATCHER_api.yaml b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/api/KAFKA_DISPATCHER_api.yaml new file mode 100644 index 0000000..9e3cecb --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/api/KAFKA_DISPATCHER_api.yaml @@ -0,0 +1,242 @@ +openapi: 3.0.0 +info: + title: 'Kafka message dispatcher for A1 interface' + version: 0.0.1 + description: | + Kafka message dispatcher server. + license: + name: Copyright (C) 2022 Nordix Foundation. Licensed under the Apache License. + url: http://www.apache.org/licenses/LICENSE-2.0 +externalDocs: + description: 'RestFUL APIs that create and dispatch Kafka messages to Kafka brokers' + url: 'https://docs.o-ran-sc.org/projects/o-ran-sc-sim-a1-interface/en/latest/index.html' +servers: + - url: '{apiRoot}' + variables: + apiRoot: + default: 'https://example.com' +paths: + '/policytypetotopicmapping/{policyTypeId}': + parameters: + - name: policyTypeId + in: path + required: true + schema: + "$ref": "#/components/schemas/PolicyTypeId" + get: + operationId: dispatcher.get_policy_type_to_topic_mapping + description: 'Get the kafka request and response topic map corresponding to policy type' + tags: + - The mapping from policy type to kafka topic request and response object + responses: + 200: + description: 'The policy type to topic map schemas' + content: + application/json: + schema: + "$ref": "#/components/schemas/PolicyTypeToTopicMap" + 404: + "$ref": "#/components/responses/404-NotFound" + 429: + "$ref": "#/components/responses/429-TooManyRequests" + 503: + "$ref": "#/components/responses/503-ServiceUnavailable" + + '/policytypes/{policyTypeId}/kafkadispatcher/{policyId}': + parameters: + - name: policyTypeId + in: path + required: true + schema: + "$ref": "#/components/schemas/PolicyTypeId" + - name: policyId + in: path + required: true + schema: + "$ref": "#/components/schemas/A1PolicyId" + put: + operationId: dispatcher.put_policy + description: 'Dispatch create and update operation as kafka message to kafka cluster' + tags: + - Individual policy Object + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/A1PolicyObject" + responses: + 200: + description: 'Create or update operation dispatched' + 400: + "$ref": "#/components/responses/400-BadRequest" + 408: + "$ref": "#/components/responses/408-RequestTimeout" + 419: + "$ref": "#/components/responses/419-KafkaMessagePublishFailed" + 429: + "$ref": "#/components/responses/429-TooManyRequests" + 503: + "$ref": "#/components/responses/503-ServiceUnavailable" + 507: + "$ref": "#/components/responses/507-InsufficientStorage" + delete: + operationId: dispatcher.delete_policy + description: 'Dispatch policy delete opertion as kafka message to kafka cluster' + responses: + 200: + description: 'Delete operation dispatched' + 408: + "$ref": "#/components/responses/408-RequestTimeout" + 419: + "$ref": "#/components/responses/419-KafkaMessagePublishFailed" + 429: + "$ref": "#/components/responses/429-TooManyRequests" + 503: + "$ref": "#/components/responses/503-ServiceUnavailable" + + '/policytypes/{policyTypeId}/kafkadispatcher/{policyId}/status': + parameters: + - name: policyTypeId + in: path + required: true + schema: + "$ref": "#/components/schemas/PolicyTypeId" + - name: policyId + in: path + required: true + schema: + "$ref": "#/components/schemas/A1PolicyId" + get: + operationId: dispatcher.get_policy_status + description: 'Dispatch policy status query opertion as kafka message to kafka cluster' + tags: + - Individual A1 Policy Status Object + responses: + 200: + description: 'Query policy status operation dispatched' + 429: + "$ref": "#/components/responses/429-TooManyRequests" + 503: + "$ref": "#/components/responses/503-ServiceUnavailable" + +components: + schemas: + # + # Representation objects + # + A1PolicyObject: + description: 'A generic policy object' + type: object + + A1Policy: + description: 'A generic policy string' + type: string + + PolicyTypeToTopicMap: + description: 'Request and response topic map for each policy type' + type: object + properties: + policy_type: + type: object + properties: + request_topic: + type: string + example: kafkatopicreq + response_topic: + type: string + example: kafkatopicres + + 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 + + PolicyTypeId: + description: 'Policy type identifier assigned by the A1-P Provider' + 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" + + 408-RequestTimeout: + description: 'Request could not be processed in given amount of time' + 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" + + 419-KafkaMessagePublishFailed: + description: 'Publishing the kafka message to the broker gets fail' + 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/KAFKA_DISPATCHER/certificate/cert.crt b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/cert.crt new file mode 100644 index 0000000..6408f33 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/cert.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID+zCCAuOgAwIBAgIUWy7JHRvA2GfOU5op4Xcc/8wQF18wDQYJKoZIhvcNAQEL +BQAwgYwxCzAJBgNVBAYTAklFMRIwEAYDVQQIDAlXRVNUTUVBVEgxEDAOBgNVBAcM +B0FUSExPTkUxETAPBgNVBAoMCEVyaWNzc29uMQwwCgYDVQQLDANFU1QxETAPBgNV +BAMMCGVzdC50ZWNoMSMwIQYJKoZIhvcNAQkBFhRoYWxpbC5jYWthbEBlc3QudGVj +aDAeFw0yMjA2MTcwOTEwMDNaFw00OTExMDEwOTEwMDNaMIGMMQswCQYDVQQGEwJJ +RTESMBAGA1UECAwJV0VTVE1FQVRIMRAwDgYDVQQHDAdBVEhMT05FMREwDwYDVQQK +DAhFcmljc3NvbjEMMAoGA1UECwwDRVNUMREwDwYDVQQDDAhlc3QudGVjaDEjMCEG +CSqGSIb3DQEJARYUaGFsaWwuY2FrYWxAZXN0LnRlY2gwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCqwDVZ7txWX/FaiRiSVa2jnBcV7KN6eqwcKtP3cNP+ +3VTm4YtcY6yp/dPXTYqkAX1qmp5i8USFPnbCstAijI5Uy8kl63dYbirHMPwt9AOL +TXrFRrJ/sev3ULJWKB1IOGt2rFhoUXA23Hv1hagyvjx2upbnVmhrz5qBOT1wuzwN +U2PjFaCFHBs0XphFS/UDEQlvpbNz/jxwHVrEdO8Jr951OFlUBczDDGk0jJ3hRc0p +iM5LNGH02yDvE6pCqqY5Fo5aaj9Vi0Kztv1D/NClWcr3Yh3IuMyZkfS+S8nPd7Nu +VHKWo7cPv9QpeziWiqx0fZcSAh6tUF52hrwGrMONuEojAgMBAAGjUzBRMB0GA1Ud +DgQWBBRiX8NchgUa825PcmhA0b+BOnkx5TAfBgNVHSMEGDAWgBRiX8NchgUa825P +cmhA0b+BOnkx5TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAi +p6UMfLeTROQJNS4ZAkpQKEtrhGaE8DpnZW+tCLWaxkTk3Mo3ybE3rBW2zugmXFIU +K2PRKivxHmVxY/YQL3sxzQRnzRPl0wuEmm+r0LpGR6VXfTaPrT6YzKM0TU/xKqJI +INk6JiBx8v9dp5LWGoJEs0e4wvoV8Bd05u+73cbhIUisO7KmCp/u1D1BHYtBNDCp +sVH6y9mAlvMIAOzy4wOWqoAxcW2ES8JbesbLOxt/IaQO9DQFPUIjTZURG+62lNCS ++2+lb1ihNqzEECuyw1GQRt88HrSWuj99bCBRRiBij900anadoKIDHdWSEkzhBa0K +zJ5KoQK/o4szZ3VPByfh +-----END CERTIFICATE----- diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/generate_cert_and_key.sh b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/generate_cert_and_key.sh new file mode 100755 index 0000000..7e6d29c --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/generate_cert_and_key.sh @@ -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/KAFKA_DISPATCHER/certificate/key.crt b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/key.crt new file mode 100644 index 0000000..9f81115 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/key.crt @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQI/PWiKXnGNAMCAggA +MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECPdSVeuyJFIwBIIEyGmpW5lUwpKF +tlcwBj2WvKF/7GJFw9LEuIBnkm0Ya8LV5Fu5XS9OGg877OkLVr1P152YjDQ+h1nD +ZZSOEqjAGGSlEshie2DSr9bdJdBlr8MTog7SlHQsrU0QLxPRBBwqseI5cyu1Vb41 +4V5+l0iWUoDvXH3mUeH460A43GU4ZUTimkWpP9M9LHyrofjMg7sLzwvOns207DXK +0hffc5293t85CQPau1WenHsfXn65tkPFblYILpJyU4sa1kLa4f5Ynt7RH2mNztfI +26kViIuOBDXcmLAxL3WGZaR9u71qpl0wC1umXWxWNt69DRVyOf23mHHSuuDSEyM3 ++x0rrbj/QaLNnoEjAFvijEAkYdp/jPKKP3kp6LpSVvVVOGLP7srrIrOe4q6bPFfK +d5u1Vnh3/PVEf8xPbJe8UJ5cfx3mWhT+saZOIpEpQvom5GwSYt869P1xyAaa59cx +TcT9KC/Ytg7fSHwXwTclIvucD+cJvbEZNFAwxMkL94a60LNfZ/odq1Bu304Shm/V +DSNeDi1HjfoC3aca7bjsXE8Xj82JQLaSGt7+AuY3gICA5cnJxxWv5VoyRZVPsiRj +Z6ykP2ikjkaLQaqDJmpbnx2ZK/lfrkJI1yI0kYK0xApUQdx9ks8c/AeEcUby2z+H +qPJZuuh1NlEv8jSFn1CO3a5Bpq53EtQlxonKzJYHdoKm1oIEbIUE60K+1oxyXt9S ++l8PgH0T2QlM2lvipy5XegPrTuMYqDywEt4cf1Yk/8RSYEeLzcfKzSuNuy556YRd +Of9nJOKPkVr1cp5si9Vyt+t4cD826WWV1ZgEcIK0i8uhEQxHb+y88DDWAV1DJGog +M0qPGm95lWj4ESiv7A/+AbXY3rJRMp/JB1jmGTNfa3jQ9P29cTwhlpvPnhqviLRH +1YfIOJfghrF55e83bLmJcfwVktMwO8Wzovw7U/1Rzy1j/fyUNkhOEbziIu97ScD+ +7AQDfRqBZZXWREOylAmfWMqZxwVn+CytAFyTLP5CpjSKgBV+qJ5xc3GqNJxaKGK5 +ULXfJKtTheJV3YYnvCuDySFuCv/dkUVjaA3/TqTikKm4THxS+DjtFJey+KWTx57p +X+8ky/E3zAuZzP7r6Dszhp+CAfvXp4CwkLitqfbwja2lcxQ+hbpHYjLN6zvHmmOZ +o0ZeoNpsWj00jAc9NrJt08DfcNomlUz9CZgpvE64gXX7wPCw5zG/c5dbFuoImEeI +h+fKwGh2KdJwazvZ/B20/TRUn1WaQzyRiCJMIgc3aO3AiuffeurliO93iTeVCjJD +d+Bdt4Ub0zPRikTqY4PUDiyfM+vRHOkJ39dY39XaZEsLLXB6XeACWcWT6iBF56Pe +UAi+C4IwPLBHMsIVVSvH8/Rg0IFBHVmkGIi0q7gIyVTcR7FQeCzDZVAjJn4aBgXo +S57v2AbMdD3pwie4ou91NkiM/SnSimLA0BxEh1UEhZk2BEW2Yy6OuvcLn3EfeZVu +M9UNmwIFaq9jWM/qcRQa7MbXTZUTUIPsOvMOsZwPIurqtXWZZTWZ8D0eu2Hu6Vui +FGyY1xIVcIpGXesADsYwy+CSW/EjpV1s+/LGDsUcqZpMeWmj84Zh1Tjt+Fet+967 +dSViTwISZ+O8F2uq0MaiPg== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/pass b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/pass new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/certificate/pass @@ -0,0 +1 @@ +test diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER/nginx.conf b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/nginx.conf new file mode 100644 index 0000000..8de906d --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/nginx.conf @@ -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 7075; + listen [::]:7075; + 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:7777; + } + } + + server { # simple reverse-proxy + listen 7175 ssl; + listen [::]:7175 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:7777; + } + } + ## + # 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; +} diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER/resources/policytype_to_topicmap.json b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/resources/policytype_to_topicmap.json new file mode 100644 index 0000000..9c5cb9b --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/resources/policytype_to_topicmap.json @@ -0,0 +1,14 @@ +{ + "ANR": { + "request_topic": "kafkatopicreq", + "response_topic": "kafkatopicres" + }, + "STD_1": { + "request_topic": "kafkatopicreq2", + "response_topic": "kafkatopicres2" + }, + "STD_2": { + "request_topic": "kafkatopicreq3", + "response_topic": "kafkatopicres3" + } +} diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/dispatcher.py b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/dispatcher.py new file mode 100644 index 0000000..08a4eed --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/dispatcher.py @@ -0,0 +1,326 @@ +# ============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 os +import json +import time +import math + +from flask import request, Response +from datetime import datetime +from kafka.consumer.fetcher import ConsumerRecord +from kafka import TopicPartition +from var_declaration import forced_settings +from maincommon import create_kafka_producer, create_kafka_consumer, create_kafka_event, create_kafka_response_event, byte_to_str, get_random_string + + +MSG_BROKER_URL=os.getenv('MSG_BROKER_URL') + +TIME_OUT=os.getenv('TIME_OUT') +publishresponse=os.getenv('PUBLISH_RESP') + +#Constsants +APPL_JSON='application/json' +TEXT_PLAIN='text/plain' +APPL_PROB_JSON='application/problem+json' + +# API Function: Dispatch create or update events to Kafka cluster +def put_policy(policyTypeId, policyId): + + if ((r := check_modified_response()) is not None): + return r + + policy_type_id = str(policyTypeId) + policy_id = str(policyId) + + try: + # Error based unit test rel only, for more info please check basic_test_with_cust_header + req_id_from_header = request.headers.get('requestid') + # Differentiate if the PUT is update or create operation since the endpoint is the same + update_oper_from_header = request.headers.get('updateoper') + data = request.data + data = json.loads(data) + except Exception: + pjson=create_problem_json(None, "The a1policy is corrupt or missing.", 400, None, policy_id) + return Response(json.dumps(pjson), 400, mimetype=APPL_PROB_JSON) + + # Decide if the operation is update or create + if (update_oper_from_header is not None): + kafka_event = create_kafka_event(policy_type_id, policy_id, data, 'UPDATE') + else: + kafka_event = create_kafka_event(policy_type_id, policy_id, data, 'CREATE') + + # Synch callout hooks towards kafka broker + if (MSG_BROKER_URL is not None): + return publish_and_consume(kafka_event, req_id_from_header, policy_type_id) + + return Response('', 200, mimetype=TEXT_PLAIN) + + +# API Function: Dispatch delete events to south Kafka cluster +def delete_policy(policyTypeId, policyId): + + if ((r := check_modified_response()) is not None): + return r + + policy_type_id = str(policyTypeId) + policy_id = str(policyId) + + req_id_from_header = request.headers.get('requestid') + print('req_id_from_header', req_id_from_header) + + # Synch callout hooks towards kafka broker + kafka_event = create_kafka_event(policy_type_id, policy_id, None, 'DELETE') + if (MSG_BROKER_URL is not None): + return publish_and_consume(kafka_event, req_id_from_header, policy_type_id) + + return Response('', 200, mimetype=TEXT_PLAIN) + + +# API Function: Get status for a policy +def get_policy_status(policyTypeId, policyId): + + if ((r := check_modified_response()) is not None): + return r + + policy_type_id=str(policyTypeId) + policy_id=str(policyId) + + req_id_from_header = request.headers.get('requestid') + print('req_id_from_header', req_id_from_header) + + # Synch callout hooks towards kafka broker + kafka_event = create_kafka_event(policy_type_id, policy_id, None, 'GET') + if (MSG_BROKER_URL is not None): + return publish_and_consume(kafka_event, req_id_from_header, policy_type_id) + + return Response('', 200, mimetype=TEXT_PLAIN) + + +def get_policy_type_to_topic_mapping(policyTypeId): + + if ((r := check_modified_response()) is not None): + return r + + policy_type_id = str(policyTypeId) + + m_file = open('../resources/policytype_to_topicmap.json') + map_in_dict = json.load(m_file) + + if policy_type_id in map_in_dict.keys(): + topic_address = map_in_dict[policy_type_id] + return Response(json.dumps(topic_address), 200, mimetype=APPL_JSON) + else: + pjson=create_problem_json(None, "The policy type to topic mapping does not exist.", 404, None, policy_type_id) + return Response(json.dumps(pjson), 404, mimetype=APPL_PROB_JSON) + + +# Helper: Publishes and consumes (to/from) the target broker and the topic in two-way synch +def publish_and_consume(kafka_event, req_id_from_header, pol_type_id): + + # Instantiate KafkaProducer with keyword arguments + producer = create_kafka_producer() + + # Assigns an id to each request that is supposed to get a result + # if a req_id already exists in req headers, it means that test generated req_id is in use for testing only + if (req_id_from_header is None): + req_id = get_random_string(16) + else: + req_id = req_id_from_header + + try: + + resp = get_policy_type_to_topic_mapping(pol_type_id) + # if the policy type to topic mapping could not be found, then returns 404 + # else gets target topic to publish the message to + if (resp.status_code == 404): + return resp + else: + data = json.loads(resp.data) + target_topic_req = data['request_topic'] + target_topic_res = data['response_topic'] + + # synch-publish + # KafkaProducer.send(topicname, value=broker_message, key=req_id, headers=None, partition=None, timestamp_ms=None) + fut_rec_metadata = producer.send(target_topic_req, kafka_event, req_id) + record_metadata = fut_rec_metadata.get() + print('Future:', record_metadata) + publish_time_in_ms = record_metadata.timestamp + + # For test purposes only triggered from A1 sim + # Publish the success response event with no error-info to response topic + # It is obvious that non of the requests will have a request id in the header except the test scripts: basic_test and timeout_test + if (publishresponse is not None and req_id_from_header is None): + kafka_response_event = create_kafka_response_event(200, "") + producer.send(target_topic_res, kafka_response_event, req_id) + + # synch-consume + consumer_record = consume_record_for(req_id, target_topic_res) + if (isinstance(consumer_record, ConsumerRecord)): + + print("Consumer Record:", consumer_record) + cons_rec_value = consumer_record.value + cons_rec_val_in_dict = json.loads(cons_rec_value) + resp_code = cons_rec_val_in_dict['response-code'] + + # if response code success, then check for time-out + if (int(resp_code) == 200): + # time-out control block, default time-out duration is thirty seconds + consume_time_in_ms = consumer_record.timestamp + elapsed_time_in_ms = consume_time_in_ms - publish_time_in_ms + print('Elapsed time in ms:', elapsed_time_in_ms) + if (elapsed_time_in_ms < int(TIME_OUT) * 1000): + return Response('', 200, mimetype=APPL_JSON) + else: + # returns time-out response code + pjson=create_error_response(408) + return Response(json.dumps(pjson), 408, mimetype=APPL_PROB_JSON) + else: + # for all other responses returns special error of this module by wrapping actual resp code + pjson=create_error_response(419) + return Response(json.dumps(pjson), 419, mimetype=APPL_PROB_JSON) + + elif (isinstance(consumer_record, Response)): + # Returns time-out response + return consumer_record + else: + # returns special error of this module + pjson=create_error_response(419) + return Response(json.dumps(pjson), 419, mimetype=APPL_PROB_JSON) + + except Exception as err: + print('Error while publish and consume', err) + pjson=create_error_response(419) + return Response(json.dumps(pjson), 419, mimetype=APPL_PROB_JSON) + finally: + producer.close() + + +# Helper: Searches for req_id by seeking every five seconds up to thirty seconds +# Helper: If the req_id is found, then ConsumerRecord will be returned +# Helper: If the req_id is not found, then Response Request Timeout will be returned +def consume_record_for(req_id, target_topic_res): + + try: + print ('req_id looking for in consumer: ' + target_topic_res, req_id) + consumer = create_kafka_consumer() + topic_partition = TopicPartition(target_topic_res, 0) + consumer.assign([topic_partition]) + + sleep_period_in_sec = 5 + poll_cycle_threshold = calc_pollcycle_threshold(sleep_period_in_sec) + poll_retries = 0 + + while (poll_retries < poll_cycle_threshold): + for consumer_record in consumer: + # Get req_id as msg_key and converts it from byte to str for each consumer record + msg_key = byte_to_str(consumer_record.key) + print ('msg_key in a consumer_record:', msg_key) + if (req_id == msg_key): + print ('req_id is found in consumer records', req_id) + return consumer_record + + print('Sleeping for ' + str(sleep_period_in_sec) + ' seconds...') + time.sleep(sleep_period_in_sec) + poll_retries += 1 + + # Returns time-out response + pjson=create_error_response(408) + return Response(json.dumps(pjson), 408, mimetype=APPL_PROB_JSON) + + except Exception as err: + print('Error while consume record for req_id', err) + pjson=create_error_response(419) + return Response(json.dumps(pjson), 419, mimetype=APPL_PROB_JSON) + finally: + consumer.close() + +# Helper: calculates poll cycle threshold +def calc_pollcycle_threshold(sleep_period_in_sec): + + poll_cycle_threshold = int(TIME_OUT) / sleep_period_in_sec + poll_cycle_threshold = math.floor(poll_cycle_threshold) + return poll_cycle_threshold + +# Helper: Create a response object if forced http response code is set +def get_forced_response(): + + if (forced_settings['code'] is not None): + resp_code=forced_settings['code'] + pjson=create_error_response(int(resp_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 == 408: + return(create_problem_json(None, "Request timeout", 408, "Request timeout", 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 == 419): + return(create_problem_json(None, "Kafka message publish failed", 419, "Publishing the event could not be processed on the Kafka cluster", 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/KAFKA_DISPATCHER/src/main.py b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/main.py new file mode 100644 index 0000000..7816e7f --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/main.py @@ -0,0 +1,77 @@ +# ============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 forced_settings, app +from maincommon import check_timeout, check_apipath + +#Constants +TEXT_PLAIN='text/plain' + +check_apipath() +check_timeout() + +# 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) + +#Set|Reset force response to be returned from dispatcher +#/dispatcheradmin/forceresponse?code= +@app.route('/dispatcheradmin/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 dispatcher responses", 200, mimetype=TEXT_PLAIN) + else: + return Response("Force response code: " + str(forced_settings['code']) + " set for all dispatcher response until it is resetted", 200, mimetype=TEXT_PLAIN) + +#Set|Reset force delay response, in seconds, for all external server responses +#/a1policy/forcedelay?delay= +@app.route('/dispatcheradmin/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 dispatcher responses ", 200, mimetype=TEXT_PLAIN) + else: + return Response("Force delay: " + str(forced_settings['delay']) + " sec set for all dispatcher responses until it is resetted ", 200, mimetype=TEXT_PLAIN) + +port_number = 7777 +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('KAFKA_DISPATCHER_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/KAFKA_DISPATCHER/src/maincommon.py b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/maincommon.py new file mode 100644 index 0000000..d5b65ae --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/maincommon.py @@ -0,0 +1,123 @@ +# ============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 os +import sys +import json +from pathlib import Path +from flask import Response +import socket +import ssl +import random +import string + +from kafka import KafkaProducer, KafkaConsumer + +#Must exist +apipath=os.environ['APIPATH'] +timeout=os.getenv('TIME_OUT') + +MSG_BROKER_URL=os.getenv('MSG_BROKER_URL') + + +# Make sure the api path is set, otherwise exit +def check_apipath(): + if (apipath is None): + print("Env APIPATH not set. Exiting....") + sys.exit(1) + +# Make sure the timeout is set and greater than zero, otherwise exit +def check_timeout(): + if (timeout is None): + print("Env TIME_OUT not set. Exiting....") + sys.exit(1) + elif (int(timeout) < 0): + print("Env TIME_OUT must be greater than zero. Exiting....") + sys.exit(1) + +# Instantiate KafkaProducer with keyword arguments +# https://kafka-python.readthedocs.io/en/master/apidoc/KafkaProducer.html +def create_kafka_producer(): + + producer = KafkaProducer( + bootstrap_servers = [MSG_BROKER_URL], + key_serializer = str.encode, + value_serializer = lambda m: json.dumps(m).encode('ascii'), + ) + return producer + + +# Instantiate KafkaConsumer with keyword arguments +# https://kafka-python.readthedocs.io/en/master/apidoc/KafkaConsumer.html +def create_kafka_consumer(): + consumer = KafkaConsumer( + # kafka cluster endpoint + bootstrap_servers = MSG_BROKER_URL, + # move to the earliest or latest available message + auto_offset_reset = 'earliest', + # number of milliseconds to block during message iteration + # if no new message available during this period of time, iteration through a for-loop will stop automatically + consumer_timeout_ms = 100, + value_deserializer = lambda m: json.loads(m.decode('ascii')), + #enable_auto_commit=False + ) + return consumer + + +# Helper: Builds a Kafka event +def create_kafka_event(policy_type_id, policy_id, payload, operation): + + kafka_event_format = {'action': operation_to_action(operation), 'payload': payload, 'policy_type_id': policy_type_id, 'policy_id': policy_id} + # converts dict to str + kafka_event_json = json.dumps(kafka_event_format) + return kafka_event_json + +# Helper: Builds a Kafka event +def create_kafka_response_event(response_code, error_info): + + kafka_response_event_format = {'response-code': response_code, 'error-info': error_info} + # converts dict to str + kafka_response_event_json = json.dumps(kafka_response_event_format) + return kafka_response_event_json + +# Helper: Converts a HTTP operation to an explanation +def operation_to_action(argument): + + switcher = { + 'CREATE': "CreatePolicy", + 'UPDATE': "UpdatePolicy", + 'DELETE': "DeletePolicy", + 'GET': "GetPolicyStatus", + } + return switcher.get(argument, None) + + +# Helper: Converts a byte array to a str +def byte_to_str(byte_arr): + + if (byte_arr is not None): + return byte_arr.decode('utf-8') + else: + return None + + +# Helper: Creates random string +def get_random_string(length): + + characters = string.ascii_letters + string.digits + string.punctuation + password = ''.join(random.choice(characters) for i in range(length)) + return password diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/payload_logging.py b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/payload_logging.py new file mode 100644 index 0000000..9457d04 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/payload_logging.py @@ -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/', 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/KAFKA_DISPATCHER/src/start.sh b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/start.sh new file mode 100644 index 0000000..e4e3510 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/start.sh @@ -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 Kafka message dispatcher +echo "Path to main.py: "$PWD +python -u main.py diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/var_declaration.py b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/var_declaration.py new file mode 100644 index 0000000..e6063b4 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER/src/var_declaration.py @@ -0,0 +1,26 @@ +# ============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) + +forced_settings = {} +forced_settings['code']=None +forced_settings['delay']=None diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/basic_test.sh b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/basic_test.sh new file mode 100755 index 0000000..62c2fa5 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/basic_test.sh @@ -0,0 +1,166 @@ +#!/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 Kafka message dispatcher. +# 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=7075 + # Set http protocol + HTTPX="http" +else + #Default https port for the simulator + PORT=7175 + # Set https protocol + HTTPX="https" +fi + +. ../common/test_common.sh +. ../common/elapse_time_curl.sh + +echo "=== Kafka message dispatcher hello world ===" +RESULT="OK" +do_curl GET / 200 + +echo "=== Reset force delay ===" +RESULT="Force delay has been resetted for all dispatcher responses" +do_curl POST /dispatcheradmin/forcedelay 200 + +echo "=== API: Get policy type to topic mapping of type: ANR ===" +res=$(cat jsonfiles/ANR_to_topic_map.json) +RESULT="json:$res" +do_curl GET /policytypetotopicmapping/ANR 200 + +echo "=== Put policy: shall publish and consume for put policy operation ===" +req_id=$(get_random_number) +RESULT="" +do_curl PUT /policytypes/ANR/kafkadispatcher/alpha 200 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +publish_response_event $req_id kafkatopicres +wait $proc_id + +echo "=== Get policy status: shall publish and consume for get policy status operation ===" +req_id=$(get_random_number) +RESULT="" +do_curl GET /policytypes/ANR/kafkadispatcher/alpha/status 200 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +publish_response_event $req_id kafkatopicres +wait $proc_id + +echo "=== Put policy: shall publish and consume for put policy operation for alpha ===" +req_id=$(get_random_number) +RESULT="" +do_curl PUT /policytypes/STD_1/kafkadispatcher/alpha 200 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +publish_response_event $req_id kafkatopicres2 +wait $proc_id + +echo "=== Delete policy: shall publish and consume for delete policy operation for alpha ===" +req_id=$(get_random_number) +RESULT="" +do_curl DELETE /policytypes/STD_1/kafkadispatcher/alpha 200 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +publish_response_event $req_id kafkatopicres2 +wait $proc_id + +echo "=== Set force delay 5 sec ===" +RESULT="Force delay: 5 sec set for all dispatcher responses until it is resetted" +do_curl POST '/dispatcheradmin/forcedelay?delay=5' 200 + +echo "=== Hello world: shall wait at least sec and then respond while hello world ===" +RESULT="OK" +do_elapsetime_curl GET / 200 jsonfiles/alpha_policy.json 5 + +echo "=== Reset force delay ===" +RESULT="Force delay has been resetted for all dispatcher responses" +do_curl POST /dispatcheradmin/forcedelay 200 + +echo "=== Put policy: shall publish and consume for put policy operation for beta ===" +req_id=$(get_random_number) +RESULT="" +do_curl PUT /policytypes/STD_1/kafkadispatcher/beta 200 jsonfiles/beta_policy.json $req_id & +proc_id=$! +publish_response_event $req_id kafkatopicres2 +wait $proc_id + +echo "=== Get policy status: shall publish and consume for get policy status operation ===" +req_id=$(get_random_number) +RESULT="" +do_curl GET /policytypes/ANR/kafkadispatcher/alpha/status 200 jsonfiles/beta_policy.json $req_id & +proc_id=$! +publish_response_event $req_id kafkatopicres +wait $proc_id + +echo "=== Put policy: shall publish and consume for put policy operation for alpha ===" +req_id=$(get_random_number) +RESULT="" +do_curl PUT /policytypes/STD_2/kafkadispatcher/alpha 200 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +publish_response_event $req_id kafkatopicres3 +wait $proc_id + +echo "=== Set force response code: 500 ===" +RESULT="Force response code: 500 set for all dispatcher response until it is resetted" +do_curl POST '/dispatcheradmin/forceresponse?code=500' 200 + +echo "=== Put policy: shall not publish and consume for put policy operation for alpha ===" +req_id=$(get_random_number) +res=$(cat jsonfiles/forced_response.json) +RESULT="json:$res" +do_curl PUT /policytypes/ANR/kafkadispatcher/alpha 500 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +publish_response_event $req_id kafkatopicres +wait $proc_id + +echo "=== Reset force response code ===" +RESULT="Force response code has been resetted for dispatcher responses" +do_curl POST /dispatcheradmin/forceresponse 200 + +echo "=== Get policy status: shall publish and consume for get policy status operation ===" +req_id=$(get_random_number) +RESULT="" +do_curl GET /policytypes/ANR/kafkadispatcher/alpha/status 200 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +publish_response_event $req_id kafkatopicres +wait $proc_id + +echo "=== Delete policy: shall publish and consume for delete policy operation for alpha ===" +req_id=$(get_random_number) +RESULT="" +do_curl DELETE /policytypes/STD_1/kafkadispatcher/alpha 200 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +publish_response_event $req_id kafkatopicres2 +wait $proc_id + +echo "********************" +echo "*** All tests ok ***" +echo "********************" diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/build_and_start.sh b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/build_and_start.sh new file mode 100755 index 0000000..f0869ee --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/build_and_start.sh @@ -0,0 +1,53 @@ +#!/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 kafka dispatcher container +# Make sure to run the simulator including args as is this script + +print_usage() { + echo "Usage: ./build_and_start.sh publish-resp|ignore-publish" + exit 1 +} + +if [ $# -ne 1 ]; then + print_usage +fi + +if [ $1 == "publish-resp" ]; then + PUBLISH_RESP="-e PUBLISH_RESP=1" +elif [ $1 == "ignore-publish" ]; then + PUBLISH_RESP="" +else + print_usage +fi + +echo "Building Kafka message dispatcher image..." +cd ../KAFKA_DISPATCHER/ + +#Build the image +docker build -t kafka_dispatcher . + +docker stop kafkamessagedispatcher > /dev/null 2>&1 +docker rm -f kafkamessagedispatcher > /dev/null 2>&1 + +echo "Starting Kafka message dispatcher..." +echo "PWD path: "$PWD + +#Run the container in interactive mode with host networking driver which allows docker to access localhost, unsecure port 7075, secure port 7175, TIME_OUT must be in seconds, PUBLISH_RESP decides auto responding for testing that run by A1 sim +docker run --network host --rm -it -p 7075:7075 -p 7175:7175 -e ALLOW_HTTP=true -e MSG_BROKER_URL=localhost:9092 -e TIME_OUT=30 $PUBLISH_RESP --volume "$PWD/certificate:/usr/src/app/cert" --name kafkamessagedispatcher kafka_dispatcher diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/ANR_to_topic_map.json b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/ANR_to_topic_map.json new file mode 100644 index 0000000..ed52462 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/ANR_to_topic_map.json @@ -0,0 +1,4 @@ +{ + "request_topic": "kafkatopicreq", + "response_topic": "kafkatopicres" +} diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/alpha_policy.json b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/alpha_policy.json new file mode 100644 index 0000000..66c2b63 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/alpha_policy.json @@ -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/KAFKA_DISPATCHER_TEST/jsonfiles/beta_policy.json b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/beta_policy.json new file mode 100644 index 0000000..a61c7fc --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/beta_policy.json @@ -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/KAFKA_DISPATCHER_TEST/jsonfiles/forced_response.json b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/forced_response.json new file mode 100644 index 0000000..4d26325 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/forced_response.json @@ -0,0 +1,5 @@ +{ + "title": "Unknown", + "status": 500, + "detail": "Not implemented response code" +} diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/timeout_response.json b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/timeout_response.json new file mode 100644 index 0000000..dd034c4 --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/jsonfiles/timeout_response.json @@ -0,0 +1,5 @@ +{ + "title": "Request timeout", + "status": 408, + "detail": "Request timeout" +} diff --git a/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/timeout_test.sh b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/timeout_test.sh new file mode 100755 index 0000000..f4e080c --- /dev/null +++ b/near-rt-ric-simulator/test/KAFKA_DISPATCHER_TEST/timeout_test.sh @@ -0,0 +1,139 @@ +#!/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 error testing of the Kafka message dispatcher +# The timeout value should be equal to TIME_OUT param that exist in the start script +# Run the script with the args: nonsecure|secure timeout=30 + +print_usage() { + echo "Usage: ./basic_test.sh nonsecure|secure timeout=30" + exit 1 +} + +if [ $# -ne 2 ]; then + print_usage +fi +if [ "$1" != "nonsecure" ] && [ "$1" != "secure" ]; then + print_usage +fi + +timeout=$(echo "$2" | cut -d'=' -f2) +regexp_for_number='^[0-9]+$' + +if ! [[ $timeout =~ $regexp_for_number ]] ; then + echo "error:"$timeout" Not a number" + exit 1 +else + if [ $timeout -le 0 ]; then + echo "Timeout value must be greater than zero" + exit 1 + fi +fi + +if [ $1 == "nonsecure" ]; then + # Default http port for the simulator + PORT=7075 + # Set http protocol + HTTPX="http" +else + #Default https port for the simulator + PORT=7175 + # Set https protocol + HTTPX="https" +fi + +. ../common/test_common.sh + +echo "=== Kafka message dispatcher hello world ===" +RESULT="OK" +do_curl GET / 200 + +echo "=== Reset force delay ===" +RESULT="Force delay has been resetted for all dispatcher responses" +do_curl POST /dispatcheradmin/forcedelay 200 + +# asynch error test case +echo "=== Put policy: shall publish and consume time-out ===" +req_id=$(get_random_number) +res=$(cat jsonfiles/timeout_response.json) +RESULT="json:$res" +# asynch callout +do_curl PUT /policytypes/ANR/kafkadispatcher/alpha 408 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +sleep $timeout +# after time out duration, publish the event +publish_response_event $req_id kafkatopicres +# wait until the main process to be completed +wait $proc_id + +# asynch success test case after 10s +echo "=== Put policy: shall publish and consume success at least 10 secs later ===" +req_id=$(get_random_number) +RESULT="" +# asynch callout +do_curl PUT /policytypes/STD_1/kafkadispatcher/alpha 200 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +sleep 10 +# after 10s, publish the event +publish_response_event $req_id kafkatopicres2 +# wait until the main process to be completed +wait $proc_id + +# asynch error test case +echo "=== Get policy status: shall publish and consume time-out ===" +req_id=$(get_random_number) +res=$(cat jsonfiles/timeout_response.json) +RESULT="json:$res" +# asynch callout +do_curl GET /policytypes/STD_2/kafkadispatcher/alpha/status 408 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +sleep $timeout +# after time out duration, publish the event +publish_response_event $req_id kafkatopicres3 +# wait until the main process to be completed +wait $proc_id + +# asynch success test case after 10s +echo "=== Get policy status: shall publish and consume success at least 15 secs later ===" +req_id=$(get_random_number) +RESULT="" +# asynch callout +do_curl GET /policytypes/ANR/kafkadispatcher/alpha/status 200 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +sleep 15 +# after 15s, publish the event +publish_response_event $req_id kafkatopicres +# wait until the main process to be completed +wait $proc_id + +# asynch success test case without any delay +echo "=== Delete policy: shall publish and consume success ===" +req_id=$(get_random_number) +RESULT="" +# asynch callout +do_curl DELETE /policytypes/STD_1/kafkadispatcher/alpha 200 jsonfiles/alpha_policy.json $req_id & +proc_id=$! +publish_response_event $req_id kafkatopicres2 +# wait until the main process to be completed +wait $proc_id + + +echo "********************" +echo "*** All tests ok ***" +echo "********************" diff --git a/near-rt-ric-simulator/test/STD_2.0.0/build_and_start_with_kafka.sh b/near-rt-ric-simulator/test/STD_2.0.0/build_and_start_with_kafka.sh new file mode 100755 index 0000000..eae4e37 --- /dev/null +++ b/near-rt-ric-simulator/test/STD_2.0.0/build_and_start_with_kafka.sh @@ -0,0 +1,102 @@ +#!/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 duplicate-check|ignore-duplicate kafka-srv|kafka-srv-secure publish-resp|ignore-publish" + exit 1 +} + +if [ $# -ne 3 ]; then + print_usage +fi + +if [ $1 == "duplicate-check" ]; then + DUP_CHECK=1 +elif [ $1 == "ignore-duplicate" ]; then + DUP_CHECK=0 +else + print_usage +fi + +if [ $2 == "kafka-srv" ]; then + URL="http://localhost:7075" +elif [ $2 == "kafka-srv-secure" ]; then + URL="https://localhost:7175" +else + print_usage +fi + +if [ $3 == "publish-resp" ]; then + PUBLISH_RESP="-e PUBLISH_RESP=1" +elif [ $3 == "ignore-publish" ]; then + PUBLISH_RESP="" +else + print_usage +fi + +URL_FLAG="" +if [ ! -z "$URL" ]; then + URL_FLAG="-e KAFKA_DISPATCHER_URL=$URL" +fi + +# Stop and remove container images if they run + +echo "Stopping A1 simulator image..." +docker stop a1StdSimulator > /dev/null 2>&1 +docker rm -f a1StdSimulator > /dev/null 2>&1 + +echo "Stopping kafka dispatcher server image..." +docker stop kafkamessagedispatcher > /dev/null 2>&1 +docker rm -f kafkamessagedispatcher > /dev/null 2>&1 + +# Initialize path variables for certificate and build operations + +dirstd2=$PWD + +cd ../../ +dirnrtsim=$PWD + +cd test/KAFKA_DISPATCHER/ +dirkafkasrv=$PWD + +# Build containers + +cd $dirnrtsim +echo "Building A1 simulator image..." +docker build -t a1test . + +if [ ! -z "$URL" ]; then + cd $dirkafkasrv + echo "Building kafka server image..." + docker build -t kafka_dispatcher . +fi + +# Run containers + +# Runs kafka server in detached mode +# In order to tail logs use:: docker logs -f kafkamessagedispatcher +if [ ! -z "$URL" ]; then + docker run -d --network host --rm -it -p 7075:7075 -p 7175:7175 -e ALLOW_HTTP=true -e MSG_BROKER_URL=localhost:9092 -e TIME_OUT=30 $PUBLISH_RESP --volume "$dirkafkasrv/certificate:/usr/src/app/cert" --name kafkamessagedispatcher kafka_dispatcher +fi + +# Runs A1 simulator +docker run --network host --rm -it -p 8085:8085 -p 8185:8185 -e A1_VERSION=STD_2.0.0 -e ALLOW_HTTP=true -e REMOTE_HOSTS_LOGGING=1 -e DUPLICATE_CHECK=$DUP_CHECK $URL_FLAG --volume "$dirnrtsim/certificate:/usr/src/app/cert" --name a1StdSimulator a1test diff --git a/near-rt-ric-simulator/test/common/consume_events_from_kafka_bus.py b/near-rt-ric-simulator/test/common/consume_events_from_kafka_bus.py new file mode 100644 index 0000000..f7dfb65 --- /dev/null +++ b/near-rt-ric-simulator/test/common/consume_events_from_kafka_bus.py @@ -0,0 +1,125 @@ +# ============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 is a script for test-purposes only +# It consumes a response-event from a kafka bus with different apporaches +# In order to use this script, you must have an venv for Python and kafka-python libs has to be installed +# To instal kafka-python please use: pip install kafka-python +# Example of an response-event json +#{ + #"response-code": "400", + #"error-info": "Bad format" +#} + + +import os +import json +import sys +import math +import time + +from kafka import KafkaConsumer, TopicPartition +from threading import RLock + +# Response string with JSON format +response_data_JSON = """ +{ + "response-code": 200, + "error-info": "" +} +""" + +# in seconds +TIME_OUT=30 +target_topic_res='kafkatopicres' +MSG_BROKER_URL='localhost:9092' + +# Instantiate KafkaConsumer with keyword arguments +# https://kafka-python.readthedocs.io/en/master/apidoc/KafkaConsumer.html +def create_kafka_consumer(): + consumer = KafkaConsumer( + # kafka cluster endpoint + bootstrap_servers = MSG_BROKER_URL, + # move to the earliest or latest available message + auto_offset_reset = 'earliest', + # number of milliseconds to block during message iteration + # if no new message available during this period of time, iteration through a for-loop will stop automatically + consumer_timeout_ms = 100, + value_deserializer = lambda m: json.loads(m.decode('ascii')), + #enable_auto_commit=False + ) + return consumer + +# Helper: Searches for req_id by seeking every five seconds up to thirty seconds +# Helper: If the req_id is found, then ConsumerRecord will be returned +# Helper: If the req_id is not found, then Response Request Timeout will be returned +def consume(req_id): + + try: + print ('req_id looking for in consumer:', req_id) + consumer = create_kafka_consumer() + # Latch to target topic and partition + topic_partition = TopicPartition(target_topic_res, 0) + consumer.assign([topic_partition]) + + sleep_period_in_sec = 5 + poll_cycle_threshold = calc_pollcycle_threshold(sleep_period_in_sec) + poll_retries = 0 + + while (poll_retries < poll_cycle_threshold): + for consumer_record in consumer: + # Get req_id as msg_key and converts it from byte to str for each consumer record + msg_key = byte_to_str(consumer_record.key) + print ('msg_key in a consumer_record:', msg_key) + if (req_id == msg_key): + print ('req_id is found in consumer records', req_id) + return consumer_record + + print('Sleeping for ' + str(sleep_period_in_sec) + ' seconds...') + time.sleep(sleep_period_in_sec) + poll_retries += 1 + + return 1 + except Exception as err: + print('Error while consume record for req_id', err) + return 1 + finally: + consumer.close() + +# Helper: calculates poll cycle threshold +def calc_pollcycle_threshold(sleep_period_in_sec): + + poll_cycle_threshold = int(TIME_OUT) / sleep_period_in_sec + poll_cycle_threshold = math.floor(poll_cycle_threshold) + return poll_cycle_threshold + +# Helper: Converts a byte array to a str +def byte_to_str(byte_arr): + + if (byte_arr is not None): + return byte_arr.decode('utf-8') + else: + return None + +if __name__ == '__main__': + try: + requestid = sys.argv[1] + future = consume(requestid) + except Exception as err: + print('Error in __main__', err) + print (1) + sys.exit() diff --git a/near-rt-ric-simulator/test/common/publish_response_event_to_kafka_bus.py b/near-rt-ric-simulator/test/common/publish_response_event_to_kafka_bus.py new file mode 100644 index 0000000..251ba09 --- /dev/null +++ b/near-rt-ric-simulator/test/common/publish_response_event_to_kafka_bus.py @@ -0,0 +1,88 @@ +# ============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 script publishes a response-event to a kafka bus +# In order to use this script, you must have an venv for Python and kafka-python libs has to be installed +# To instal kafka-python please use: pip install kafka-python +# Example of an response-event json +#{ + #"response-code": "400", + #"error-info": "Bad format" +#} + + +import os +import json +import sys + +from kafka import KafkaProducer + +# Response string with JSON format +response_data_JSON = """ +{ + "response-code": 200, + "error-info": "" +} +""" + +# Instantiate KafkaProducer with keyword arguments +# https://kafka-python.readthedocs.io/en/master/apidoc/KafkaProducer.html +def create_kafka_producer(): + + producer = KafkaProducer( + bootstrap_servers = ['localhost:9092'], + key_serializer = str.encode, + value_serializer = lambda m: json.dumps(m).encode('ascii'), + ) + return producer + +# Helper: Publishes (to) the target broker and the topic in synch +def publish(kafka_evet, req_id, targettopic): + + # Instantiate KafkaProducer with keyword arguments + producer = create_kafka_producer() + # Assigns an id to each request that is supposed to get a result + # req_id = 'Hll1EsycKLNRric7' + + try: + + # synch-publish + # KafkaProducer.send(topicname, value=broker_message, key=req_id, headers=None, partition=None, timestamp_ms=None) + fut_rec_metadata = producer.send(targettopic, kafka_evet, req_id) + return fut_rec_metadata.get() + + except Exception as err: + print('Error while publish', err) + finally: + producer.close() + +if __name__ == '__main__': + try: + + requestid = sys.argv[1] + targettopic = sys.argv[2] + # response_data_JSON is str + future = publish(response_data_JSON, requestid, targettopic) + + if (future is not None): + print (0) + else: + print (1) + + except Exception: + print (1) + sys.exit() diff --git a/near-rt-ric-simulator/test/common/test_common.sh b/near-rt-ric-simulator/test/common/test_common.sh index 6d2f80e..e1191d2 100755 --- a/near-rt-ric-simulator/test/common/test_common.sh +++ b/near-rt-ric-simulator/test/common/test_common.sh @@ -1,7 +1,7 @@ #!/bin/bash # ============LICENSE_START=============================================== -# Copyright (C) 2020 Nordix Foundation. All rights reserved. +# Copyright (C) 2020-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. @@ -26,6 +26,7 @@ #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. +#The requestid parameter is being introduced in the fifth order. do_curl() { if [ $# -lt 3 ]; then echo "Need 3 or more parameters, [file]: "$@ @@ -33,9 +34,12 @@ do_curl() { exit 1 fi curlstr="curl -X "$1" -skw %{http_code} $HTTPX://localhost:"${PORT}${2}" -H accept:*/*" - if [ $# -gt 3 ]; then + if [ $# -eq 4 ]; then curlstr=$curlstr" -H Content-Type:application/json --data-binary @"$4 fi + if [ $# -ge 5 ]; then + curlstr=$curlstr" -H Content-Type:application/json --data-binary @"$4" -H requestid:"$5 + fi echo " CMD (${BASH_LINENO[0]}):"$curlstr res=$($curlstr) status=${res:${#res}-3} @@ -75,6 +79,33 @@ do_curl() { fi } +# Triggers publish_event_to_kafka_bus.py script to send msg to Kafka broker +# The aim of this function is to realize error related test cases only +# The request_id for the Kafka msg, should be passed here as a function parameter +publish_response_event() { + if [ $# -ne 2 ]; then + echo "Need 2 parameter, " + echo "Exiting test script....." + exit 1 + fi + res=$(python ../common/publish_response_event_to_kafka_bus.py "$1" "$2") + if [ $res -eq 0 ]; then + echo " Result as expected " + else + echo " Result not expected " + echo " Exiting..... " + exit 1 + fi +} + +# Creates 16 digits random number using letters and numbers only +get_random_number() { + r_num=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 16) + echo $r_num +} + +# It is being used to cross-test-cases in between A1 sim and external server +# The parameter it holds all with regards to External Server relates e.g. HTTPX_EXT_SRV and PORT_EXT_SRV do_curl_ext_srv() { if [ $# -lt 3 ]; then echo "Need 3 or more parameters, [file]: "$@ diff --git a/tox.ini b/tox.ini index 8250799..8a3aaa6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ # ================================================================================== -# Copyright (c) 2020 Nordix +# Copyright (c) 2020-2022 Nordix # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -48,23 +48,15 @@ commands = # doc jobs [testenv:docs] whitelist_externals = echo -basepython = python3 -deps = - sphinx - sphinx-rtd-theme - sphinxcontrib-httpdomain - recommonmark - lfdocs-conf +basepython = python3.8 +deps = -r{toxinidir}/docs/requirements-docs.txt commands = sphinx-build -W -b html -n -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/html echo "Generated docs available in {toxinidir}/docs/_build/html" [testenv:docs-linkcheck] skipsdist = true -basepython = python3 -deps = sphinx - sphinx-rtd-theme - sphinxcontrib-httpdomain - recommonmark - lfdocs-conf -commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/linkcheck +basepython = python3.8 +deps = -r{toxinidir}/docs/requirements-docs.txt +commands = + sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/linkcheck -- 2.16.6