From f6314546c2e7c5992882fff9756ddf510151c338 Mon Sep 17 00:00:00 2001 From: ychacon Date: Fri, 10 Feb 2023 14:07:50 +0100 Subject: [PATCH] Updates for G Maintenance release Introduce setup.py to create pip distributions, and update README.md. Run & fix the result of local Sonarcube scan. Introduce test suite for unit test cases, implement automaic way of finding the tests Implement unit test for logging Add synchronization for data structure Update release notes Fix to expose the port outside the container Issue-ID: NONRTRIC-838 Signed-off-by: ychacon Change-Id: Ide0aa7980f85bf4f06e268564628fd0ab5f05530 --- catalogue-enhanced-test/build_and_start.sh | 14 ++- catalogue-enhanced/.gitignore | 4 + catalogue-enhanced/Dockerfile | 1 + catalogue-enhanced/README.md | 102 ++++++++++++++------- .../api/rapp-catalogue-enhanced.yaml | 7 ++ catalogue-enhanced/config/logger.yaml | 49 ++++++++++ catalogue-enhanced/pyproject.toml | 25 +++++ catalogue-enhanced/setup.py | 45 +++++++++ catalogue-enhanced/src/catalogue_manager.py | 99 +++++++++----------- catalogue-enhanced/src/configuration/log_config.py | 39 ++++++++ .../src/{ => configuration}/payload_logging.py | 0 catalogue-enhanced/src/main.py | 24 +++-- catalogue-enhanced/src/maincommon.py | 14 +-- .../src/repository/synchronized_rapp_registry.py | 64 +++++++++++++ catalogue-enhanced/src/repository/tosca_meta.py | 54 +++++++++++ catalogue-enhanced/src/start.sh | 10 +- catalogue-enhanced/src/util.py | 47 ---------- catalogue-enhanced/src/var_declaration.py | 11 ++- catalogue-enhanced/tests/suite.py | 49 ++++++++++ catalogue-enhanced/tests/test_log_config.py | 70 ++++++++++++++ .../tests/test_sychronized_rapp_registry.py | 78 ++++++++++++++++ catalogue-enhanced/tests/test_tosca_meta.py | 51 +++++++++++ catalogue-enhanced/tests/unittest_setup.py | 1 + 23 files changed, 699 insertions(+), 159 deletions(-) create mode 100644 catalogue-enhanced/config/logger.yaml create mode 100644 catalogue-enhanced/pyproject.toml create mode 100644 catalogue-enhanced/setup.py create mode 100644 catalogue-enhanced/src/configuration/log_config.py rename catalogue-enhanced/src/{ => configuration}/payload_logging.py (100%) create mode 100644 catalogue-enhanced/src/repository/synchronized_rapp_registry.py create mode 100644 catalogue-enhanced/src/repository/tosca_meta.py delete mode 100644 catalogue-enhanced/src/util.py create mode 100644 catalogue-enhanced/tests/suite.py create mode 100644 catalogue-enhanced/tests/test_log_config.py create mode 100644 catalogue-enhanced/tests/test_sychronized_rapp_registry.py create mode 100644 catalogue-enhanced/tests/test_tosca_meta.py diff --git a/catalogue-enhanced-test/build_and_start.sh b/catalogue-enhanced-test/build_and_start.sh index 351f1df..e303a31 100755 --- a/catalogue-enhanced-test/build_and_start.sh +++ b/catalogue-enhanced-test/build_and_start.sh @@ -21,11 +21,19 @@ # Make sure to run container including args as is this script print_usage() { - echo "Usage: ./build_and_start.sh" + echo "Usage: ./build_and_start.sh logger-dev|logger-prod" exit 1 } -if [ $# -ge 1 ]; then +if [ $# -ne 1 ]; then + print_usage +fi + +if [ $1 == "logger-dev" ]; then + ACTIVE_LOGGER="-e ACTIVE_LOGGER=dev" +elif [ $1 == "logger-prod" ]; then + ACTIVE_LOGGER="-e ACTIVE_LOGGER=prod" +else print_usage fi @@ -42,4 +50,4 @@ echo "Starting rapp catalogue enhanced..." echo "PWD path: "$PWD #Run the container in interactive mode with host networking driver which allows docker to access localhost, unsecure port 9096, secure port 9196 -docker run --network host --rm -it -p 9096:9096 -p 9196:9196 -e ALLOW_HTTP=true --volume "$PWD/certificate:/usr/src/app/cert" --name rappcatalogueenhanced rapp_catalogue_enhanced_image +docker run --network host --rm -it -p 9096:9096 -p 9196:9196 -e ALLOW_HTTP=true $ACTIVE_LOGGER --volume "$PWD/certificate:/usr/src/app/cert" --name rappcatalogueenhanced rapp_catalogue_enhanced_image diff --git a/catalogue-enhanced/.gitignore b/catalogue-enhanced/.gitignore index 00f2c95..3309556 100644 --- a/catalogue-enhanced/.gitignore +++ b/catalogue-enhanced/.gitignore @@ -14,3 +14,7 @@ htmlcov/ # Python virtual env venv/ + +# Local Sonarqube scan folder +.scannerwork/ + diff --git a/catalogue-enhanced/Dockerfile b/catalogue-enhanced/Dockerfile index 390dcc5..db33be5 100644 --- a/catalogue-enhanced/Dockerfile +++ b/catalogue-enhanced/Dockerfile @@ -29,6 +29,7 @@ COPY nginx.conf nginx.conf COPY certificate /usr/src/app/cert COPY src src COPY csar csar +COPY config config ARG user=nonrtric ARG group=nonrtric diff --git a/catalogue-enhanced/README.md b/catalogue-enhanced/README.md index 2b493e7..83c8c0a 100644 --- a/catalogue-enhanced/README.md +++ b/catalogue-enhanced/README.md @@ -1,6 +1,6 @@ ## License -Copyright (C) 2022 Nordix Foundation. +Copyright (C) 2022-2023 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 @@ -13,85 +13,123 @@ 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. -# O-RAN-SC Non-RT RIC rAPP Catalogue Enhanced +# O-RAN-SC Non-RT RIC rApp Catalogue Enhanced The O-RAN Non-RT RIC rApp Catalogue Enhanced provides an OpenApi 3.0 REST API for services to register/unregister themselves and discover other services. -The O-RAN Non-RT RIC rApp Catalogue Enhanced module supports GET, PUT and DELETE operations (version of the OpenAPI yaml file): +The O-RAN Non-RT RIC rApp Catalogue Enhanced module supports GET, PUT and DELETE operations. For the specifications please refer to [OpenAPI.yaml] ./api/rapp-catalogue-enhanced.yaml -| Yaml file | Version | -| -----------------------------|-------------------- | -| rapp-catalogue-enhanced.yaml | 1.0.0 | +|Yaml file | Version | +|-----------------------------|-------------------- | +|rapp-catalogue-enhanced.yaml | 1.0.0 | The overall folder structure is (relative to the location of this README file): | Dir | Description | | ---------------- | ----------- | -|. |Dockerfile and README.md | -|api |The OpenApi yaml | -|src |Python source code | +|. |Dockerfile, container-tag.yaml, nginx.conf, pyproject.toml, tox.ini, setup.py, and README.md | +|api |The OpenApi yaml rapp-catalogue-enhanced.yaml | +|src |Python source codes includes sub-directories repository, configuration, and start.sh | |certificate |A self-signed certificate and a key | -|tests |Pytest fixture and test setup utility | +|config |Configuration files such as logger.yaml | +|csar |CSAR files such as csar/rapp1.csar | +|tests |Python unit tests, Pytest fixture, and test setup utility | + +The other folder structure (catalogue-enhanced-test) that includes tests implemented in bash via Curl: + +| Dir | Description | +| ---------------- | ----------- | +|. |build_and_start.sh, and basic_test.sh | +|common |compare_json.py and test_commons.sh are the utilities to compare json files | +|jsonfiles |JSON scripts related to API responses | The application is being implemented in Python programming language. -The rApp Catalogue Enhanced module handles the requests that are defined in the OpenAPI yaml file. All these requests are implemented in the catalogue_manager.py module in the src folder. In addition, a number of utility functions are also supported and implemented by the main.py and payload_logging.py in the source folder. +The rApp Catalogue Enhanced module handles the requests that are defined in the OpenAPI YAML file. All these requests are implemented in the catalogue_manager.py module in the src folder. The CRUD operations of rApps have been implemented in synchronized blocks so that multi-thread access could not lead to inconsistency of the data. In addition, the logging functions are also supported and implemented by payload_logging.py and log_config.py in the configuration folder. -The section below outlines the supported open api rest-based operations as well as the utility operations. +The section below outlines the supported OpenAPI rest-based operations as well as the utility operations. # Ports and certificates -The rApp Catalogue Enhanced module normally opens the port 9096 for http. If a certificate and a key are provided the module will open port 9196 for https instead. The port 9196 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. +The rApp Catalogue Enhanced module normally opens the port 9096 for HTTP. If a certificate and a key are provided the module will open port 9196 for HTTPS instead. The port 9196 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 | | -------- | ----- | | 9096 | http | | 9196 | 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 9096 and the https port 9196. If using curl and https, the flag -k shall be given to make curl ignore checking the certificate. +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 9096 and the https port 9196. If using CURL and HTTPS, the flag -k shall be given to make curl ignore checking the certificate. -# Supported operations in Non-RT RIC rAPP Catalogue Enhanced +# Supported operations in Non-RT RIC rApp Catalogue Enhanced -For the complete yaml specification, see [OpenAPI.yaml](../api/rapp-catalogue-enhanced.yaml) +For the complete YAML specification, see [OpenAPI.yaml](./api/rapp-catalogue-enhanced.yaml) URIs for server: | Function | Path and parameters | | --------------------- | ------------------- | -| GET, Query all rapp ids | localhost:9096/rappcatalogue | -| GET, Query rapp by rapp id | localhost:9096/rappcatalogue/ | -| GET, Query API list by rapp id and service type | localhost:9096/rappcatalogue// | -| GET, Validate and query TOSCA.meta file content by rapp id | localhost:9096/rappcatalogue/csar//toscameta | -| PUT, Register rapp | localhost:9096/rappcatalogue/ | -| DELETE, Unregister rapp | localhost:9096/rappcatalogue/ | +|GET, Query all rapp ids | localhost:9096/rappcatalogue | +|GET, Query rapp by rapp id | localhost:9096/rappcatalogue/ | +|GET, Query API list by rapp id and service type | localhost:9096/rappcatalogue// | +|GET, Validate and query TOSCA.meta file content by rapp id | localhost:9096/rappcatalogue/csar//toscameta | +|PUT, Register rapp | localhost:9096/rappcatalogue/ | +|DELETE, Unregister rapp | localhost:9096/rappcatalogue/ | # Admin functions | Function | Path and parameters | | --------------------- | ------------------- | -| POST, Delete all existing rapp definitions | localhost:9096/deleteall | +|POST, Delete all existing rapp definitions | localhost:9096/deleteall | # Start and test of the Non-RT RIC rAPP Catalogue Enhanced First, download the plt/rappcatalogue repo on gerrit: + git clone "https://gerrit.o-ran-sc.org/r/a/nonrtric/plt/rappcatalogue" -Goto the main directory, 'rappcatalogue/catalogue-enhanced-test'. -This folder contains a script to build and start the rAPP Catalogue Enhanced (as a container in interactive mode), a script for basic testing as well as json files for the test script. +Goto the main directory, 'rappcatalogue/catalogue-enhanced-test'. This folder contains a script to build and start the rApp Catalogue Enhanced (as a container in interactive mode), a script for basic testing as well as JSON files for the test script. + +Note that test can be performed both using the nonsecure HTTP port and the secure HTTPS port. -Note that test can be performed both using the nonsecure http port and the secure https port. +# Building the rApp Catalogue Enhanced Build and start the rApp catalogue enhanced containers: ./build_and_start.sh -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 9096 for http and 9196 for https. When running the rapp catalogue enhanced as a container, the defualt ports can be re-mapped to any port on the localhost. +This will build and start the container in interactive mode. The built container only resides in the local docker repository. When running the rApp Catalogue Enhanced as a container, the defualt ports can be re-mapped to any port on the localhost. + +# API Testing of rApp Catalogue Enhanced + +In a second terminal, go to the same folder and run the basic test script: + basic_test.sh nonsecure|secure. + +This script runs a number of API tests towards the rApp Catalogue Enhanced to make sure it works properply. + +# Unit Testing of rApp Catalogue Enhanced + +In order to run unit test cases, there is no need to build, and start any container. However, Python's venv must exist. You can follow the below steps to create a venv: + 1- Change current directory to project's root directory that is rappcatalogue. + 2- Run the commands consecutively: + python3 -m venv venv --prompt="rappcatalogue" + source venv/bin/activate + pip install connexion + 3- Change current directory to 'catalogue-enhanced/tests/' + 4- Run the command below: + python suite.py +The suite.py will detect existing unit test cases, and run them all. + +# Installing the pip distribution of rApp Catalogue Enhanced + +It is also possible to have a pip distro of rApp catalogue. In order to install in your venv, you have to first install a venv mentioned in Unit Testing of rApp. + +Then, you can follow the below steps: + 1- Change current directory to 'rappcatalogue/catalogue-enhanced' where you can find setup.py + 2- Run the command: + pip install . + +This will build the rApp catalogue on your local. -In a second terminal, go to the same folder and run the basic test script, basic_test.sh nonsecure|secure. -This script runs a number of tests towards the rapp catalogue enhanced to make sure it works properply. diff --git a/catalogue-enhanced/api/rapp-catalogue-enhanced.yaml b/catalogue-enhanced/api/rapp-catalogue-enhanced.yaml index 262bcce..cb194b2 100644 --- a/catalogue-enhanced/api/rapp-catalogue-enhanced.yaml +++ b/catalogue-enhanced/api/rapp-catalogue-enhanced.yaml @@ -290,3 +290,10 @@ components: application/problem+json: schema: "$ref": "#/components/schemas/ProblemDetails" + + 512-ToscaMetaNotValid: + description: 'TOSCA.meta content is not valid' + content: + application/problem+json: + schema: + "$ref": "#/components/schemas/ProblemDetails" diff --git a/catalogue-enhanced/config/logger.yaml b/catalogue-enhanced/config/logger.yaml new file mode 100644 index 0000000..cf95696 --- /dev/null +++ b/catalogue-enhanced/config/logger.yaml @@ -0,0 +1,49 @@ +# ============LICENSE_START=============================================== +# Copyright (C) 2023 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================================================= +# +version: 1 + +disable_existing_loggers: True + +formatters: + extended: + format: '%(asctime)-20s :: %(levelname)-8s :: [%(process)d]%(processName)s :: %(threadName)s[%(thread)d] :: %(pathname)s :: %(lineno)d :: %(message)s' + simple: + format: '%(asctime)s :: %(name)s :: %(levelname)s :: %(message)s' + +handlers: + console: + class: logging.StreamHandler + level: DEBUG + formatter: extended + stream: ext://sys.stdout + file: + class : logging.handlers.RotatingFileHandler + level: INFO + formatter: extended + filename: /tmp/rapp_manager.log + maxBytes: 10485760 # 10MB + encoding: utf8 + +loggers: + dev: + handlers: [console, file] + prod: + handlers: [console] + +root: + level: DEBUG + handlers: [console] diff --git a/catalogue-enhanced/pyproject.toml b/catalogue-enhanced/pyproject.toml new file mode 100644 index 0000000..5c41678 --- /dev/null +++ b/catalogue-enhanced/pyproject.toml @@ -0,0 +1,25 @@ +# ============LICENSE_START=============================================== +# Copyright (C) 2023 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================================================= +# + +# pyproject.toml +[tool.pytest.ini_options] +pythonpath = [ + ".", "src", "config", +] +testpaths = [ + "tests", +] diff --git a/catalogue-enhanced/setup.py b/catalogue-enhanced/setup.py new file mode 100644 index 0000000..0c82586 --- /dev/null +++ b/catalogue-enhanced/setup.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +# ============LICENSE_START=============================================== +# Copyright (C) 2023 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 setuptools import setup + +setup(name='rappcatalogue-enhanced', + version='1.0.0', + description='The O-RAN Non-RT RIC rApp Catalogue Enhanced provides an OpenApi 3.0 REST API for rApp services to register/unregister themselves and discover other services', + maintainer='Halil Cakal', + maintainer_email='halil.cakal@est.tech', + url='https://gerrit.o-ran-sc.org/r/admin/repos/nonrtric/plt/rappcatalogue,general', + license='Apache License, Version 2.0', + platforms='any', + packages=[ + 'src', + 'src.configuration', + 'src.repository', + 'api', + 'certificate', + 'config', + 'csar', + 'tests' + ], + zip_safe=False, + include_package_data=True, + install_requires=[ + 'connexion[swagger-ui]', + ], +) diff --git a/catalogue-enhanced/src/catalogue_manager.py b/catalogue-enhanced/src/catalogue_manager.py index a318303..599d4ad 100644 --- a/catalogue-enhanced/src/catalogue_manager.py +++ b/catalogue-enhanced/src/catalogue_manager.py @@ -20,10 +20,10 @@ import json from flask import request, Response from jsonschema import validate -from var_declaration import rapp_registry +from var_declaration import synchronized_rapp_registry from zipfile import ZipFile from io import TextIOWrapper -from util import ToscametaFormatChecker +from repository.tosca_meta import ToscaMeta # Constsants APPL_JSON='application/json' @@ -33,21 +33,23 @@ APPL_PROB_JSON='application/problem+json' # API Function: Query for all rapp identifiers def query_all_rapp_ids(): - res = list(rapp_registry.keys()) + allkeys= synchronized_rapp_registry.get_rapps_keys() + res= list(allkeys) return (res, 200) # API Function: Get a rapp definition def query_rapp_by_id(rappid): - rapp_id = str(rappid) + rapp_id= str(rappid) + rapp_definition= synchronized_rapp_registry.get_rapp(rapp_id) - if (rapp_id not in rapp_registry.keys()): + if rapp_definition: + return Response(json.dumps(rapp_definition), 200, mimetype=APPL_JSON) + else: pjson=create_problem_json(None, "The rapp does not exist.", 404, None, rapp_id) return Response(json.dumps(pjson), 404, mimetype=APPL_PROB_JSON) - return Response(json.dumps(rapp_registry[rapp_id]), 200, mimetype=APPL_JSON) - # API Function: Register, or update, a rapp definition def register_rapp(rappid): @@ -61,14 +63,8 @@ def register_rapp(rappid): pjson=create_problem_json(None, "The rapp definition is corrupt or missing.", 400, None, rapp_id) return Response(json.dumps(pjson), 400, mimetype=APPL_PROB_JSON) - return_code = 201 - if rapp_id in rapp_registry.keys(): - return_code = 200 - - # Register or update rapp definition - rapp_registry[rapp_id] = data - - return Response(json.dumps(data), return_code, mimetype=APPL_JSON) + response_code= synchronized_rapp_registry.set_rapp(rapp_id, data) + return Response(json.dumps(data), response_code, mimetype=APPL_JSON) # API Function: Unregister a rapp from catalogue @@ -76,14 +72,12 @@ def unregister_rapp(rappid): rapp_id = str(rappid) - if (rapp_id not in rapp_registry.keys()): + if synchronized_rapp_registry.del_rapp(rapp_id): + return Response('', 204, mimetype=APPL_JSON) + else: pjson = create_problem_json(None, "The rapp definition does not exist.", 404, None, rapp_id) return Response(json.dumps(pjson), 404, mimetype=APPL_PROB_JSON) - # Delete rapp definition - del rapp_registry[rapp_id] - - return Response('', 204, mimetype=APPL_JSON) # API Function: Query api list by rapp_id and service_type: produced or consumed def query_api_list_by_rapp_id_and_service_type(rappid, servicetype): @@ -91,46 +85,46 @@ def query_api_list_by_rapp_id_and_service_type(rappid, servicetype): rapp_id = str(rappid) service_type = str(servicetype) - if (rapp_id in rapp_registry.keys()): + rapp_definition= synchronized_rapp_registry.get_rapp(rapp_id) - rapp_definition = rapp_registry[rapp_id] + if rapp_definition: try: arr_api_list = rapp_definition['apiList'] arr_filtered_api_list = [arr_item for arr_item in arr_api_list if arr_item['serviceType'] == service_type] return (arr_filtered_api_list, 200) - except Exception as err: - print('An error occured:', err) + except Exception: pjson=create_problem_json(None, "The rapp definition is corrupt or missing.", 400, None, rapp_id) return Response(json.dumps(pjson), 400, mimetype=APPL_PROB_JSON) return ([], 200) + # API Function: Validate and return TOSCA.meta file content def query_tosca_meta_content_by_rapp_id(rappid): rapp_id = str(rappid) - if (rapp_id not in rapp_registry.keys()): - pjson=create_problem_json(None, "The rapp does not exist.", 404, None, rapp_id) - return Response(json.dumps(pjson), 404, mimetype=APPL_PROB_JSON) + if synchronized_rapp_registry.get_rapp(rapp_id): + with open_zip_and_filter('/usr/src/app/csar/rapp1/rapp1.csar') as tosca_file: + tosca_meta = [] + while True: + line = tosca_file.readline() # Get next line from file + if not line: # end of file is reached + break + else: + tosca_meta.append(line.strip()) - with open_zip_and_filter('/usr/src/app/csar/rapp1/rapp1.csar') as tosca_file: - tosca_meta = [] - while True: - line = tosca_file.readline() # Get next line from file - if not line: # end of file is reached - break - else: - tosca_meta.append(line.strip()) - - print('TOSCA.meta content:', tosca_meta) - is_valid = validate_tosca_meta_format(tosca_meta) + try: + validate_tosca_meta_format(tosca_meta) + return Response(json.dumps(tosca_meta), 200, mimetype=APPL_JSON) + except Exception as err: + pjson=create_problem_json(None, err, 512, None, rapp_id) + return Response(json.dumps(pjson), 512, mimetype=APPL_PROB_JSON) - if is_valid == True: - content = tosca_meta - return Response(json.dumps(content), 200, mimetype=APPL_JSON) + else: + pjson=create_problem_json(None, "The rapp does not exist.", 404, None, rapp_id) + return Response(json.dumps(pjson), 404, mimetype=APPL_PROB_JSON) - return ([], 200) # Helper: Open CSAR zip file and returns TOSCA.meta def validate_tosca_meta_format(toscameta): @@ -140,16 +134,9 @@ def validate_tosca_meta_format(toscameta): csar_tag = split_and_strip(toscameta[1]) crby_tag = split_and_strip(toscameta[2]) - checker = ToscametaFormatChecker(file_tag, csar_tag, crby_tag) # util.py: validater - result = checker.validate() - - if result == True: # Log: Validated or NOT - print('Validated:', checker) - else: - print('NOT Validated:', checker) - return result - - return False + ToscaMeta(file_tag, csar_tag, crby_tag) + else: + raise ValueError("More lines than expected in Tosca.meta") # Helper: Splits given string by colon and strip def split_and_strip(string): @@ -165,11 +152,7 @@ def open_zip_and_filter(filename): for file_name in file_names: if file_name.endswith('TOSCA.meta'): return TextIOWrapper(zip_object.open(file_name)) # TextIOWrapper: provides buffered text stream - - pjson=create_problem_json(None, "TOSCA.meta file is corrupt or missing.", 400, None, rapp_id) - return Response(json.dumps(pjson), 400, mimetype=APPL_PROB_JSON) - except Exception as err: - print('An error occured:', err) + except Exception: pjson=create_problem_json(None, "The CSAR zip content is corrupt or missing.", 400, None, rapp_id) return Response(json.dumps(pjson), 400, mimetype=APPL_PROB_JSON) finally: @@ -211,5 +194,7 @@ def create_error_response(code): return(create_problem_json(None, "Service unavailable", 503, "The provider is currently unable to handle the request due to a temporary overload", 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 == 512: + return(create_problem_json(None, "Tosca.meta not valid", 512, "TOSCA.meta content is not valid", None)) else: return(create_problem_json(None, "Unknown", code, "Not implemented response code", None)) diff --git a/catalogue-enhanced/src/configuration/log_config.py b/catalogue-enhanced/src/configuration/log_config.py new file mode 100644 index 0000000..0840bf2 --- /dev/null +++ b/catalogue-enhanced/src/configuration/log_config.py @@ -0,0 +1,39 @@ +# ============LICENSE_START=============================================== +# Copyright (C) 2023 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 logging +import logging.config +import os +import yaml + +from maincommon import active_logger_profile + +log= logging.getLogger(active_logger_profile) + +class Logger: + + def __init__(self, log_cfg_path= None): + if log_cfg_path and os.path.exists(log_cfg_path): + with open(log_cfg_path, 'r') as cfg_file: + try: + config = yaml.safe_load(cfg_file.read()) + logging.config.dictConfig(config) + log.debug('Logging config has initialized with config path: %s and profile: %s', log_cfg_path, active_logger_profile) + except Exception as e: + log.exception('Error with log-config file: ', e) + else: + log.debug('No config file found, using default logger with profile: %s', active_logger_profile) diff --git a/catalogue-enhanced/src/payload_logging.py b/catalogue-enhanced/src/configuration/payload_logging.py similarity index 100% rename from catalogue-enhanced/src/payload_logging.py rename to catalogue-enhanced/src/configuration/payload_logging.py diff --git a/catalogue-enhanced/src/main.py b/catalogue-enhanced/src/main.py index 978a050..5dda795 100644 --- a/catalogue-enhanced/src/main.py +++ b/catalogue-enhanced/src/main.py @@ -1,5 +1,5 @@ # ============LICENSE_START=============================================== -# Copyright (C) 2022 Nordix Foundation. All rights reserved. +# Copyright (C) 2022-2023 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. @@ -16,15 +16,21 @@ # import sys +import os from flask import Response, Flask -from var_declaration import app, rapp_registry +from var_declaration import app, synchronized_rapp_registry +from configuration.log_config import Logger -# app var need to be initialized -import payload_logging +# App var need to be initialized +import configuration.payload_logging # Constants TEXT_PLAIN='text/plain' +LOG_CONFIG_REL_PATH= '/config/logger.yaml' + +# Base path for the container as working directory +WORKDIR= os.environ['WORKDIR'] # Check alive function @app.route('/', methods=['GET']) @@ -34,7 +40,7 @@ def test(): # Delete all rapp definitions @app.route('/deleteall', methods=['POST']) def delete_all(): - rapp_registry.clear() + synchronized_rapp_registry.clear_rapps() return Response("All rapp definitions deleted", 200, mimetype=TEXT_PLAIN) @@ -42,8 +48,14 @@ port_number = 9696 if len(sys.argv) >= 2 and isinstance(sys.argv[1], int): port_number = sys.argv[1] -#Import base RESTFul API functions from Open API +# Import base RESTFul API functions from Open API app.add_api('rapp-catalogue-enhanced.yaml') if __name__ == '__main__': + + if WORKDIR is not None: + Logger(WORKDIR+LOG_CONFIG_REL_PATH) + else: + Logger() + app.run(port=port_number, host="0.0.0.0", threaded=False) diff --git a/catalogue-enhanced/src/maincommon.py b/catalogue-enhanced/src/maincommon.py index 20370f1..64c713e 100644 --- a/catalogue-enhanced/src/maincommon.py +++ b/catalogue-enhanced/src/maincommon.py @@ -1,5 +1,5 @@ # ============LICENSE_START=============================================== -# Copyright (C) 2022 Nordix Foundation. All rights reserved. +# Copyright (C) 2022-2023 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. @@ -16,13 +16,9 @@ # import os -import sys -# Must exist -apipath = os.environ['APIPATH'] +# Get ACTIVE_LOGGER env variable, otherwise use the default logger profile +active_logger_profile= os.getenv('ACTIVE_LOGGER', 'prod') -# Make sure the api path for the open api yaml file is set, otherwise exit -def check_apipath(): - if (apipath is None): - print("Env APIPATH not set. Exiting....") - sys.exit(1) +# Get APIPATH env variable, otherwise use the default path +apipath= os.getenv('APIPATH', '/usr/src/app/api') diff --git a/catalogue-enhanced/src/repository/synchronized_rapp_registry.py b/catalogue-enhanced/src/repository/synchronized_rapp_registry.py new file mode 100644 index 0000000..7331b16 --- /dev/null +++ b/catalogue-enhanced/src/repository/synchronized_rapp_registry.py @@ -0,0 +1,64 @@ +# ============LICENSE_START=============================================== +# Copyright (C) 2023 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 threading import RLock +import time +import logging + +from maincommon import active_logger_profile + +log= logging.getLogger(active_logger_profile) + +class SychronizedRappRegistry: + + def __init__(self): + self.lock= RLock() + self._rapps= {} + + def set_rapp(self, rapp_id, data): + with self.lock: + log.debug('Acquired a lock in set_rapp for the rapp: %s', rapp_id) + if rapp_id in self._rapps.keys(): + self._rapps[rapp_id]= data + return 200 + else: + self._rapps[rapp_id]= data + return 201 + + def del_rapp(self, rapp_id): + with self.lock: + log.debug('Acquired a lock in del_rapp for the rapp: %s', rapp_id) + if rapp_id in self._rapps.keys(): + del self._rapps[rapp_id] + return rapp_id + + def get_rapp(self, rapp_id): + with self.lock: + log.debug('Acquired a lock in get_rapp for the rapp: %s', rapp_id) + if rapp_id in self._rapps.keys(): + return self._rapps[rapp_id] + + def clear_rapps(self): + with self.lock: + log.debug('Acquired a lock in clear_rapps') + if self._rapps.keys(): + self._rapps.clear() + + def get_rapps_keys(self): + with self.lock: + log.debug('Acquired a lock in get_rapps_keys') + return self._rapps.keys() diff --git a/catalogue-enhanced/src/repository/tosca_meta.py b/catalogue-enhanced/src/repository/tosca_meta.py new file mode 100644 index 0000000..5164e14 --- /dev/null +++ b/catalogue-enhanced/src/repository/tosca_meta.py @@ -0,0 +1,54 @@ +# ============LICENSE_START=============================================== +# Copyright (C) 2022-2023 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================================================= +# + + +class ToscaMeta: + + """ + A utility class aims to check required fields and their format + in TOSCA.meta file in accordance with TOSCA specs. + """ + + TOSCA= 'TOSCA-Meta-File-Version' + CSAR= 'CSAR-Version' + CREATEDBY= 'Created-By' + + def __init__(self, file_tag, csar_tag, createdby_tag): + self.file_tag = self.is_valid_file_tag(file_tag) + self.csar_tag = self.is_valid_csar_tag(csar_tag) + self.createdby_tag = self.is_valid_createdby_tag(createdby_tag) + + def is_valid_file_tag(self, file_tag): + if file_tag!= ToscaMeta.TOSCA: + raise ValueError("File tag should be like TOSCA-Meta-File-Version.") + + return file_tag + + def is_valid_csar_tag(self, csar_tag): + if csar_tag!= ToscaMeta.CSAR: + raise ValueError("CSAR tag should be like CSAR-Version.") + + return csar_tag + + def is_valid_createdby_tag(self, createdby_tag): + if createdby_tag!= ToscaMeta.CREATEDBY: + raise ValueError("Created tag should like Created-By.") + + return createdby_tag + + def __str__(self): # __str__: returns display string + return '[TOSCA.meta: %s, %s, %s]' % (self.file_tag, self.csar_tag, self.createdby_tag) diff --git a/catalogue-enhanced/src/start.sh b/catalogue-enhanced/src/start.sh index e425208..6b98bb9 100644 --- a/catalogue-enhanced/src/start.sh +++ b/catalogue-enhanced/src/start.sh @@ -1,7 +1,7 @@ #!/bin/bash # ============LICENSE_START=============================================== -# Copyright (C) 2022 Nordix Foundation. All rights reserved. +# Copyright (C) 2022-2023 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. @@ -21,6 +21,14 @@ export APIPATH=$PWD/api echo "APIPATH set to: "$APIPATH +# Set PYTHONPATH for module import +export PYTHONPATH=$PWD/src +echo "PYTHONPATH set to: "$PYTHONPATH + +# Set WORKDIR for the container +export WORKDIR=$PWD +echo "WORKDIR set to: "$WORKDIR + cd src # Start nginx diff --git a/catalogue-enhanced/src/util.py b/catalogue-enhanced/src/util.py deleted file mode 100644 index 31261fd..0000000 --- a/catalogue-enhanced/src/util.py +++ /dev/null @@ -1,47 +0,0 @@ -# ============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================================================= -# - -class ToscametaFormatChecker: - """ - A utility class aims to check required fields and their format - in TOSCA.meta file in accordance with TOSCA specs. - """ - def __init__(self, file_ver, csar_ver, created_by): - self.file_ver = file_ver - self.csar_ver = csar_ver - self.created_by = created_by - - def validate(self): - - if (self.file_ver == 'TOSCA-Meta-File-Version' and - self.csar_ver == 'CSAR-Version' and - self.created_by == 'Created-By'): - - return True - else: - return False - - def __str__(self): # __str__: returns display string - return '[TOSCA.meta: %s, %s, %s]' % (self.file_ver, self.csar_ver, self.created_by) - -# Unit tests for class: ToscametaFormatChecker -if __name__ == '__main__': - toscameta_t = ToscametaFormatChecker('TOSCA-Meta-File-Version', 'CSAR-Version', 'Created-By') - assert toscameta_t.validate() is True - - toscameta_f = ToscametaFormatChecker('TOSCA-Meta-File-Versn', 'CSAR-Version', 'Created-By') - assert toscameta_f.validate() is False diff --git a/catalogue-enhanced/src/var_declaration.py b/catalogue-enhanced/src/var_declaration.py index e455fe6..8ae6276 100644 --- a/catalogue-enhanced/src/var_declaration.py +++ b/catalogue-enhanced/src/var_declaration.py @@ -1,5 +1,5 @@ # ============LICENSE_START=============================================== -# Copyright (C) 2022 Nordix Foundation. All rights reserved. +# Copyright (C) 2022-2023 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. @@ -15,10 +15,13 @@ # ============LICENSE_END================================================= # -from maincommon import apipath import connexion +from maincommon import apipath +from repository.synchronized_rapp_registry import SychronizedRappRegistry + #Main app -app = connexion.App(__name__, specification_dir=apipath) +app= connexion.App(__name__, specification_dir=apipath) + +synchronized_rapp_registry= SychronizedRappRegistry() -rapp_registry = {} diff --git a/catalogue-enhanced/tests/suite.py b/catalogue-enhanced/tests/suite.py new file mode 100644 index 0000000..7d82b20 --- /dev/null +++ b/catalogue-enhanced/tests/suite.py @@ -0,0 +1,49 @@ +# ============LICENSE_START=============================================== +# Copyright (C) 2023 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================================================= +# + +# Combining all unit tests in one place + +import os +import sys +import unittest + +# Includes project source files +def setup_env(): + cwd=os.getcwd()+"/" + sys.path.append(os.path.abspath(cwd+'../src')) + +# Returns unit test cases from test modules +def suite(): + setup_env() + suite= unittest.TestSuite() + + modules_to_test= [] + files= os.listdir('.') + for afile in files: + if afile.startswith('test') and afile.endswith('.py') and afile!= 'test_catalogue_enhanced.py': + modules_to_test.append(afile.replace('.py', '')) + + for module in map(__import__, modules_to_test): + tests= unittest.findTestCases(module) + for test in tests: + suite.addTest(test) + + return suite + +if __name__ == '__main__': + runner= unittest.TextTestRunner() + runner.run(suite()) diff --git a/catalogue-enhanced/tests/test_log_config.py b/catalogue-enhanced/tests/test_log_config.py new file mode 100644 index 0000000..2e79c2c --- /dev/null +++ b/catalogue-enhanced/tests/test_log_config.py @@ -0,0 +1,70 @@ +# ============LICENSE_START=============================================== +# Copyright (C) 2023 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 unittest +import logging +import logging.config +import os +import yaml + +from configuration.log_config import Logger + +LOG_CONFIG_PATH= '../config/logger.yaml' + +class TestLogConfig(unittest.TestCase): + """ + Unit tests for TestLogConfig class + """ + + def test_log_config_does_not_exist_then_use_root_logger(self): + Logger() + + logger= logging.getLogger() + self.assertEqual('root', logger.name) + self.assertEqual(logging.WARNING, logger.level) + + def test_log_config_exist_then_logger_dev_exist(self): + Logger(LOG_CONFIG_PATH) + + logger= logging.getLogger('dev') + self.assertEqual('dev', logger.name) + + def test_log_config_exist_then_logger_dev_handlers_exist(self): + Logger(LOG_CONFIG_PATH) + + logger= logging.getLogger('dev') + handlers= [handler.name for handler in logger.handlers] + + self.assertEqual(['console', 'file'], handlers) + + def test_log_config_exist_then_check_logger_prod_exists(self): + Logger(LOG_CONFIG_PATH) + + logger= logging.getLogger('prod') + self.assertEqual('prod', logger.name) + + def test_log_config_exist_then_logger_prod_handlers_exist(self): + Logger(LOG_CONFIG_PATH) + + logger= logging.getLogger('prod') + handlers= [handler.name for handler in logger.handlers] + + self.assertEqual(['console'], handlers) + +if __name__ == '__main__': + unittest.main() + diff --git a/catalogue-enhanced/tests/test_sychronized_rapp_registry.py b/catalogue-enhanced/tests/test_sychronized_rapp_registry.py new file mode 100644 index 0000000..bd79e77 --- /dev/null +++ b/catalogue-enhanced/tests/test_sychronized_rapp_registry.py @@ -0,0 +1,78 @@ +# ============LICENSE_START=============================================== +# Copyright (C) 2023 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 unittest + +from threading import Thread +from repository.synchronized_rapp_registry import SychronizedRappRegistry + +class TestSynchronizedRappRegistry(unittest.TestCase): + """ + Unit tests for SychronizedRappRegistry.py + """ + + def setUp(self): + """setUp() runs before each test cases""" + self.synch_registry = SychronizedRappRegistry() + for i in range(0, 100): + # add to the dict + self.synch_registry.set_rapp(i, 'rapp'+str(i)) + + def test_synch_registry_setup_size(self): + self.assertEqual(100, len(self.synch_registry._rapps)) + + def test_synch_registry_delete(self): + for i in range(0, 100): + # Create three threads for each element in the base dict, and try concurrent delete + threads = [Thread(target=self.synch_registry.del_rapp(i)) for _ in range(3)] + # start threads + for thread in threads: + thread.start() + # wait for threads to finish + for thread in threads: + thread.join() + self.assertEqual(0, len(self.synch_registry._rapps)) + + def test_synch_registry_set(self): + for i in range(0, 100): + # Create three threads for each element in the base dict, and try concurrent set + threads = [Thread(target=self.synch_registry.set_rapp(i, 'rapp'+str(i))) for _ in range(3)] + # start threads + for thread in threads: + thread.start() + # wait for threads to finish + for thread in threads: + thread.join() + # The size of base dict should stay same + self.assertEqual(100, len(self.synch_registry._rapps)) + self.assertEqual('rapp1', self.synch_registry.get_rapp(1)) + self.assertEqual('rapp99', self.synch_registry.get_rapp(99)) + + def test_synch_registry_clear(self): + # Create three threads for clear_base + threads = [Thread(target=self.synch_registry.clear_rapps()) for _ in range(3)] + # start threads + for thread in threads: + thread.start() + # wait for threads to finish + for thread in threads: + thread.join() + # The size of base dict should be zero + self.assertEqual(0, len(self.synch_registry._rapps)) + +if __name__ == '__main__': + unittest.main() diff --git a/catalogue-enhanced/tests/test_tosca_meta.py b/catalogue-enhanced/tests/test_tosca_meta.py new file mode 100644 index 0000000..5132757 --- /dev/null +++ b/catalogue-enhanced/tests/test_tosca_meta.py @@ -0,0 +1,51 @@ +# ============LICENSE_START=============================================== +# Copyright (C) 2023 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 unittest +import logging +import logging.config +import os +import yaml + +from repository.tosca_meta import ToscaMeta + +class TestToscaMeta(unittest.TestCase): + + """ + Unit tests for ToscaMeta class + """ + + def test_toscameta_throws_exception_due_invalid_file_tag(self): + with self.assertRaises(ValueError) as ve: + ToscaMeta('TOSCA-Meta-File-Versio', 'CSAR-Version', 'Created-By') + + self.assertEqual(str(ve.exception), 'File tag should be like TOSCA-Meta-File-Version.') + + def test_toscameta_throws_exception_due_invalid_csar_tag(self): + with self.assertRaises(ValueError) as ve: + ToscaMeta('TOSCA-Meta-File-Version', 'CSAR-ersion', 'Created-By') + + self.assertEqual(str(ve.exception), 'CSAR tag should be like CSAR-Version.') + + def test_toscameta_throws_exception_due_invalid_createdby_tag(self): + with self.assertRaises(ValueError) as ve: + ToscaMeta('TOSCA-Meta-File-Version', 'CSAR-Version', 'CreatedBy') + + self.assertEqual(str(ve.exception), 'Created tag should like Created-By.') + +if __name__ == '__main__': + unittest.main() diff --git a/catalogue-enhanced/tests/unittest_setup.py b/catalogue-enhanced/tests/unittest_setup.py index e14d3ae..d3ccdba 100644 --- a/catalogue-enhanced/tests/unittest_setup.py +++ b/catalogue-enhanced/tests/unittest_setup.py @@ -38,6 +38,7 @@ def setup_env(): testdata = cwd+"../../catalogue-enhanced-test/jsonfiles/" #Env var to setup version and host logging + os.environ['WORKDIR'] = cwd os.environ['APIPATH'] = cwd+"../api/" os.environ['REMOTE_HOSTS_LOGGING'] = "ON" -- 2.16.6