From b2dee20acbe763855bed3899192fab27284faa46 Mon Sep 17 00:00:00 2001 From: "halil.cakal" Date: Mon, 17 Oct 2022 09:20:52 +0100 Subject: [PATCH] Rapp catalogue enhanced Rapp LCM Issue-ID: NONRTRIC-800 Change-Id: I6fe48b5208baf61d6920a095a2980b34ac539a1a Signed-off-by: halil.cakal --- .gitignore | 1 + catalogue-enhanced-test/basic_test.sh | 177 +++++++++++++ catalogue-enhanced-test/build_and_start.sh | 45 ++++ catalogue-enhanced-test/common/compare_json.py | 126 +++++++++ catalogue-enhanced-test/common/test_common.sh | 80 ++++++ catalogue-enhanced-test/jsonfiles/rapp1.json | 24 ++ .../jsonfiles/rapp1_invoker_apilist.json | 6 + .../jsonfiles/rapp1_provider_apilist.json | 11 + catalogue-enhanced-test/jsonfiles/rapp2.json | 24 ++ .../jsonfiles/rapp2_invoker_apilist.json | 6 + .../jsonfiles/rapp2_provider_apilist.json | 11 + catalogue-enhanced-test/jsonfiles/tosca_meta.json | 11 + catalogue-enhanced/.gitignore | 16 ++ catalogue-enhanced/Dockerfile | 48 ++++ catalogue-enhanced/README.md | 96 +++++++ .../api/rapp-catalogue-enhanced.yaml | 292 +++++++++++++++++++++ catalogue-enhanced/certificate/cert.crt | 24 ++ .../certificate/generate_cert_and_key.sh | 26 ++ catalogue-enhanced/certificate/key.crt | 30 +++ catalogue-enhanced/certificate/pass | 1 + catalogue-enhanced/csar/rapp1/rapp1.csar | Bin 0 -> 54766 bytes catalogue-enhanced/nginx.conf | 110 ++++++++ catalogue-enhanced/src/catalogue_manager.py | 215 +++++++++++++++ catalogue-enhanced/src/main.py | 49 ++++ catalogue-enhanced/src/maincommon.py | 28 ++ catalogue-enhanced/src/payload_logging.py | 60 +++++ catalogue-enhanced/src/start.sh | 31 +++ catalogue-enhanced/src/util.py | 47 ++++ catalogue-enhanced/src/var_declaration.py | 24 ++ .../tests/test_catalogue_enhanced.py | 250 ++++++++++++++++++ catalogue-enhanced/tests/unittest_setup.py | 57 ++++ catalogue-enhanced/tox.ini | 34 +++ 32 files changed, 1960 insertions(+) create mode 100755 catalogue-enhanced-test/basic_test.sh create mode 100755 catalogue-enhanced-test/build_and_start.sh create mode 100644 catalogue-enhanced-test/common/compare_json.py create mode 100755 catalogue-enhanced-test/common/test_common.sh create mode 100644 catalogue-enhanced-test/jsonfiles/rapp1.json create mode 100644 catalogue-enhanced-test/jsonfiles/rapp1_invoker_apilist.json create mode 100644 catalogue-enhanced-test/jsonfiles/rapp1_provider_apilist.json create mode 100644 catalogue-enhanced-test/jsonfiles/rapp2.json create mode 100644 catalogue-enhanced-test/jsonfiles/rapp2_invoker_apilist.json create mode 100644 catalogue-enhanced-test/jsonfiles/rapp2_provider_apilist.json create mode 100644 catalogue-enhanced-test/jsonfiles/tosca_meta.json create mode 100644 catalogue-enhanced/.gitignore create mode 100644 catalogue-enhanced/Dockerfile create mode 100644 catalogue-enhanced/README.md create mode 100644 catalogue-enhanced/api/rapp-catalogue-enhanced.yaml create mode 100644 catalogue-enhanced/certificate/cert.crt create mode 100755 catalogue-enhanced/certificate/generate_cert_and_key.sh create mode 100644 catalogue-enhanced/certificate/key.crt create mode 100644 catalogue-enhanced/certificate/pass create mode 100755 catalogue-enhanced/csar/rapp1/rapp1.csar create mode 100644 catalogue-enhanced/nginx.conf create mode 100644 catalogue-enhanced/src/catalogue_manager.py create mode 100644 catalogue-enhanced/src/main.py create mode 100644 catalogue-enhanced/src/maincommon.py create mode 100644 catalogue-enhanced/src/payload_logging.py create mode 100644 catalogue-enhanced/src/start.sh create mode 100644 catalogue-enhanced/src/util.py create mode 100644 catalogue-enhanced/src/var_declaration.py create mode 100644 catalogue-enhanced/tests/test_catalogue_enhanced.py create mode 100644 catalogue-enhanced/tests/unittest_setup.py create mode 100644 catalogue-enhanced/tox.ini diff --git a/.gitignore b/.gitignore index be1dfec..2b6a724 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ coverage.* .swagger-codegen-ignore .swagger-codegen/ api/README.md +*.pyc # Python virtual env venv/ diff --git a/catalogue-enhanced-test/basic_test.sh b/catalogue-enhanced-test/basic_test.sh new file mode 100755 index 0000000..8cad83a --- /dev/null +++ b/catalogue-enhanced-test/basic_test.sh @@ -0,0 +1,177 @@ +#!/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 rapp catalogue enhanced container +# Run the basic_test with either nonsecure or secure parameter + +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 rapp catalogue enhanced + PORT=9096 + # Set http protocol + HTTPX="http" +else + #Default https port for the rapp catalogue enhanced + PORT=9196 + # Set https protocol + HTTPX="https" +fi + +. ./common/test_common.sh + +echo "=== Rapp catalogue enhanced health check ===" +RESULT="OK" +do_curl GET '/' 200 + +echo "=== Reset rapp catalogue enhanced ===" +RESULT="All rapp definitions deleted" +do_curl POST '/deleteall' 200 + +echo "=== API: Query all rapp ids, shall be empty array ===" +RESULT="json:[]" +do_curl GET '/rappcatalogue' 200 + +echo "=== API: Query rapp by rapp id , rapp rapp1 not found ===" +RESULT="json:{\"title\": \"The rapp does not exist.\", \"status\": 404, \"instance\": \"rapp1\"}" +do_curl GET '/rappcatalogue/rapp1' 404 + +echo "=== API: Register an rapp: rapp1 ===" +res=$(cat jsonfiles/rapp1.json) +RESULT="json:$res" +do_curl PUT '/rappcatalogue/rapp1' 201 jsonfiles/rapp1.json + +echo "=== API: Query all rapp ids, shall contain rapp id rapp1 ===" +RESULT="json:[ \"rapp1\" ]" +do_curl GET '/rappcatalogue' 200 + +echo "=== API: Query rapp by rapp id, rapp rapp1 found ===" +res=$(cat jsonfiles/rapp1.json) +RESULT="json:$res" +do_curl GET '/rappcatalogue/rapp1' 200 + +echo "=== API: Filter api list by service type and rapp id, service type provider ===" +res=$(cat jsonfiles/rapp1_provider_apilist.json) +RESULT="json:$res" +do_curl GET '/rappcatalogue/rapp1/provider' 200 + +echo "=== API: Filter api list by service type and rapp id, service type invoker ===" +res=$(cat jsonfiles/rapp1_invoker_apilist.json) +RESULT="json:$res" +do_curl GET '/rappcatalogue/rapp1/invoker' 200 + +echo "=== API: Delete rapp by rapp id, rapp rapp1 deleted successfully ===" +RESULT="" +do_curl DELETE '/rappcatalogue/rapp1' 204 + +echo "=== API: Query all rapp ids, shall be empty array ===" +RESULT="json:[]" +do_curl GET '/rappcatalogue' 200 + +echo "=== API: Query rapp by rapp id , rapp rapp1 not found ===" +RESULT="json:{\"title\": \"The rapp does not exist.\", \"status\": 404, \"instance\": \"rapp1\"}" +do_curl GET '/rappcatalogue/rapp1' 404 + +echo "=== API: Register an rapp: rapp1 ===" +res=$(cat jsonfiles/rapp1.json) +RESULT="json:$res" +do_curl PUT '/rappcatalogue/rapp1' 201 jsonfiles/rapp1.json + +echo "=== API: Query all rapp ids, shall contain rapp id rapp1 ===" +RESULT="json:[ \"rapp1\" ]" +do_curl GET '/rappcatalogue' 200 + +echo "=== API: Query rapp by rapp id, rapp rapp1 found ===" +res=$(cat jsonfiles/rapp1.json) +RESULT="json:$res" +do_curl GET '/rappcatalogue/rapp1' 200 + +echo "=== API: Query TOSCA.meta file by rapp_id, TOSCA.meta verified and listed ===" +res=$(cat jsonfiles/tosca_meta.json) +RESULT="json:$res" +do_curl GET '/rappcatalogue/csar/rapp1/toscameta' 200 + +echo "=== API: Query TOSCA.meta file by rapp_id, rapp does not exist ===" +RESULT="json:{\"title\": \"The rapp does not exist.\", \"status\": 404, \"instance\": \"rapp2\"}" +do_curl GET '/rappcatalogue/csar/rapp2/toscameta' 404 + +echo "=== API: Update an rapp: rapp1 ===" +res=$(cat jsonfiles/rapp1.json) +RESULT="json:$res" +do_curl PUT '/rappcatalogue/rapp1' 200 jsonfiles/rapp1.json + +echo "=== API: Query rapp by rapp id, rapp rapp1 found ===" +res=$(cat jsonfiles/rapp1.json) +RESULT="json:$res" +do_curl GET '/rappcatalogue/rapp1' 200 + +echo "=== API: Register an rapp: rapp2 ===" +res=$(cat jsonfiles/rapp2.json) +RESULT="json:$res" +do_curl PUT '/rappcatalogue/rapp2' 201 jsonfiles/rapp2.json + +echo "=== API: Query rapp by rapp id, rapp rapp2 found ===" +res=$(cat jsonfiles/rapp2.json) +RESULT="json:$res" +do_curl GET '/rappcatalogue/rapp2' 200 + +echo "=== API: Filter api list by service type and rapp id, service type provider ===" +res=$(cat jsonfiles/rapp2_provider_apilist.json) +RESULT="json:$res" +do_curl GET '/rappcatalogue/rapp2/provider' 200 + +echo "=== API: Filter api list by service type and rapp id, service type invoker ===" +res=$(cat jsonfiles/rapp2_invoker_apilist.json) +RESULT="json:$res" +do_curl GET '/rappcatalogue/rapp2/invoker' 200 + +echo "=== API: Query all rapp ids, shall contain rapp id rapp1 and rapp2 ===" +RESULT="json:[ \"rapp1\", \"rapp2\" ]" +do_curl GET '/rappcatalogue' 200 + +echo "=== API: Delete rapp by rapp id, rapp rapp1 deleted successfully ===" +RESULT="" +do_curl DELETE '/rappcatalogue/rapp1' 204 + +echo "=== API: Query rapp by rapp id , rapp rapp1 not found ===" +RESULT="json:{\"title\": \"The rapp does not exist.\", \"status\": 404, \"instance\": \"rapp1\"}" +do_curl GET '/rappcatalogue/rapp1' 404 + +echo "=== API: Query all rapp ids, shall contain rapp id rapp1 and rapp2 ===" +RESULT="json:[ \"rapp2\" ]" +do_curl GET '/rappcatalogue' 200 + +echo "=== API: Query rapp by rapp id, rapp rapp2 found ===" +res=$(cat jsonfiles/rapp2.json) +RESULT="json:$res" +do_curl GET '/rappcatalogue/rapp2' 200 + +echo "********************" +echo "*** All tests ok ***" +echo "********************" diff --git a/catalogue-enhanced-test/build_and_start.sh b/catalogue-enhanced-test/build_and_start.sh new file mode 100755 index 0000000..351f1df --- /dev/null +++ b/catalogue-enhanced-test/build_and_start.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# ============LICENSE_START=============================================== +# Copyright (C) 2022 Nordix Foundation. All rights reserved. +# ======================================================================== +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END================================================= +# + +# Script to build and start the rapp catalogue enhanced container +# Make sure to run container including args as is this script + +print_usage() { + echo "Usage: ./build_and_start.sh" + exit 1 +} + +if [ $# -ge 1 ]; then + print_usage +fi + +echo "Building rapp catalogue image..." +cd ../catalogue-enhanced/ + +#Build the image +docker build -t rapp_catalogue_enhanced_image . + +docker stop rappcatalogueenhanced > /dev/null 2>&1 +docker rm -f rappcatalogueenhanced > /dev/null 2>&1 + +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 diff --git a/catalogue-enhanced-test/common/compare_json.py b/catalogue-enhanced-test/common/compare_json.py new file mode 100644 index 0000000..cbebd74 --- /dev/null +++ b/catalogue-enhanced-test/common/compare_json.py @@ -0,0 +1,126 @@ + +# ============LICENSE_START=============================================== +# 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. +# 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 compare two jsons for eqaulity, taken into account that the parameter values +# marked with '????' are not checked (only the parameter name need to exist) +# Example of target json with '????' +# [ +# { +# "callbackUrl": "????", +# "keepAliveIntervalSeconds": "????", +# "serviceName": "serv2", +# "timeSinceLastActivitySeconds": "????" +# }, +# { +# "callbackUrl": "????", +# "keepAliveIntervalSeconds": "????", +# "serviceName": "serv1", +# "timeSinceLastActivitySeconds": "????" +# } +#] + +import os +import json +import sys + +# # Helper function to compare two json list. +# # Returns true for equal, false for not equal +def compare_json_list(list1, list2): + if (list1.__len__() != list2.__len__()): + return False + + for l in list1: + found = False + for m in list2: + res = compare_json(l, m) + if (res): + found = True + break + + if (not found): + return False + + return True + +# Deep compare of two json obects +# If a parameter value in the target json is set to '????' then the result json value is not checked for the that parameter +# Return true for equal json, false for not equal json +def compare_json(obj1, obj2): + if isinstance(obj1, list): + if (not isinstance(obj2, list)): + return False + return compare_json_list(obj1, obj2) + elif (isinstance(obj1, dict)): + if (not isinstance(obj2, dict)): + return False + exp = set(obj2.keys()) == set(obj1.keys()) + if (not exp): + return False + for k in obj1.keys(): + val1 = obj1.get(k) + val2 = obj2.get(k) + if isinstance(val1, list): + if (not compare_json_list(val1, val2)): + return False + elif isinstance(val1, dict): + if (not compare_json(val1, val2)): + return False + else: + #Do not check parameter values marked with '????' + if ((val1 != "????") and (val2 != val1)) and ((val2 != "????") and (val2 != val1)): + return False + else: + return obj1 == obj2 + + return True + + +# Compare two json object. Returns true if equal, false if not equal +# This function is intended to be used from other python scipts using json object (instead of files) +def compare(target, result): + try: + res1=compare_json(target, result) + res2=compare_json(target, result) + if (res1 and res2): + return True + else: + return False + except Exception: + return False + +if __name__ == '__main__': + try: + #Read the input file and compare the two json (target->result) + jsonTarget = json.loads(sys.argv[1]) + jsonResult = json.loads(sys.argv[2]) + res1=compare_json(jsonTarget, jsonResult) + + #Read the json again (in case the previous calls has re-arranged the jsons) + jsonTarget = json.loads(sys.argv[1]) + jsonResult = json.loads(sys.argv[2]) + #Compare the opposite order (result->target) to catch special duplicate json key cases + res2=compare_json(jsonResult, jsonTarget) + + if (res1 and res2): + print (0) + else: + print (1) + + except Exception: + print (1) + sys.exit() diff --git a/catalogue-enhanced-test/common/test_common.sh b/catalogue-enhanced-test/common/test_common.sh new file mode 100755 index 0000000..69f45e6 --- /dev/null +++ b/catalogue-enhanced-test/common/test_common.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +# ============LICENSE_START=============================================== +# 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END================================================= +# + +# Function to execute curl and compare + print result + +# Note: Env var PORT must be set to the intended port number +# Notre Env var HTTPX must be set to either 'http' or 'https' + +#args: [file] +#Expects the env $RESULT to contain the expected RESULT. +#If json, the RESULT shall begin with 'json:'. +#Any json parameter with unknown value shall be given as "????" to skip checking the value. +#The requestid parameter is being introduced in the fifth order. +do_curl() { + if [ $# -lt 3 ]; then + echo "Need 3 or more parameters, [file]: "$@ + echo "Exiting test script....." + exit 1 + fi + curlstr="curl -X "$1" -skw %{http_code} $HTTPX://localhost:"${PORT}${2}" -H accept:*/*" + 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} + body=${res:0:${#res}-3} + if [ $status -ne $3 ]; then + echo " Error status :"$status" Expected status: "$3 + echo " Body :"$body + echo "Exiting test script....." + exit 1 + else + echo " OK, code :"$status" (Expected)" + echo " Body :"$body + if [ "$RESULT" == "*" ]; then + echo " Body contents not checked" + elif [[ "$RESULT" == "json:"* ]]; then + result=${RESULT:5:${#RESULT}} #Remove 'json:' from the result string + res=$(python ./common/compare_json.py "$result" "$body") + if [ $res -eq 0 ]; then + echo " Expected json body :"$result + echo " Body as expected" + else + echo " Expected json body :"$result + echo "Exiting....." + exit 1 + fi + else + body="$(echo $body | tr -d '\n' )" + if [ "$RESULT" == "$body" ]; then + echo " Expected body :"$RESULT + echo " Body as expected" + else + echo " Expected body :"$RESULT + echo "Exiting....." + exit 1 + fi + fi + fi +} diff --git a/catalogue-enhanced-test/jsonfiles/rapp1.json b/catalogue-enhanced-test/jsonfiles/rapp1.json new file mode 100644 index 0000000..c37765e --- /dev/null +++ b/catalogue-enhanced-test/jsonfiles/rapp1.json @@ -0,0 +1,24 @@ +{ + "supplierName": "ESY", + "rappSchema": { + "version": "1.0.0", + "display_name": "orufhrecovery", + "description": "O-RU Front-Haul Recovery", + "csarUrl": "http://localhost:9190/rapp1/csar" + }, + "apiList": [ + { + "serviceType": "invoker", + "apiId": "88918c1ccdb75a2da0f284572b66ec2c" + }, + { + "serviceType": "provider", + "apiId": "84111c1ccdb75a2da0f284572b66e854", + "aefProfiles": [ + { + "aefId": "dc76679f940d56c86987f0ba7973df0f" + } + ] + } + ] +} diff --git a/catalogue-enhanced-test/jsonfiles/rapp1_invoker_apilist.json b/catalogue-enhanced-test/jsonfiles/rapp1_invoker_apilist.json new file mode 100644 index 0000000..b666eba --- /dev/null +++ b/catalogue-enhanced-test/jsonfiles/rapp1_invoker_apilist.json @@ -0,0 +1,6 @@ +[ + { + "serviceType": "invoker", + "apiId": "88918c1ccdb75a2da0f284572b66ec2c" + } +] diff --git a/catalogue-enhanced-test/jsonfiles/rapp1_provider_apilist.json b/catalogue-enhanced-test/jsonfiles/rapp1_provider_apilist.json new file mode 100644 index 0000000..750501a --- /dev/null +++ b/catalogue-enhanced-test/jsonfiles/rapp1_provider_apilist.json @@ -0,0 +1,11 @@ +[ + { + "serviceType": "provider", + "apiId": "84111c1ccdb75a2da0f284572b66e854", + "aefProfiles": [ + { + "aefId": "dc76679f940d56c86987f0ba7973df0f" + } + ] + } +] diff --git a/catalogue-enhanced-test/jsonfiles/rapp2.json b/catalogue-enhanced-test/jsonfiles/rapp2.json new file mode 100644 index 0000000..25bbb1b --- /dev/null +++ b/catalogue-enhanced-test/jsonfiles/rapp2.json @@ -0,0 +1,24 @@ +{ + "supplierName": "ESY", + "rappSchema": { + "version": "1.0.0", + "display_name": "ransliceassurance", + "description": "Re-prioritize a RAN slice’s radio resource allocation priority if sufficient throughput cannot be maintained", + "csarUrl": "http://localhost:9190/rapp2/csar" + }, + "apiList": [ + { + "serviceType": "invoker", + "apiId": "88918c1ccdb75a2da0f284572b66ec2c" + }, + { + "serviceType": "provider", + "apiId": "84111c1ccdb75a2da0f284572b66e854", + "aefProfiles": [ + { + "aefId": "dc76679f940d56c86987f0ba7973df0f" + } + ] + } + ] +} diff --git a/catalogue-enhanced-test/jsonfiles/rapp2_invoker_apilist.json b/catalogue-enhanced-test/jsonfiles/rapp2_invoker_apilist.json new file mode 100644 index 0000000..b666eba --- /dev/null +++ b/catalogue-enhanced-test/jsonfiles/rapp2_invoker_apilist.json @@ -0,0 +1,6 @@ +[ + { + "serviceType": "invoker", + "apiId": "88918c1ccdb75a2da0f284572b66ec2c" + } +] diff --git a/catalogue-enhanced-test/jsonfiles/rapp2_provider_apilist.json b/catalogue-enhanced-test/jsonfiles/rapp2_provider_apilist.json new file mode 100644 index 0000000..750501a --- /dev/null +++ b/catalogue-enhanced-test/jsonfiles/rapp2_provider_apilist.json @@ -0,0 +1,11 @@ +[ + { + "serviceType": "provider", + "apiId": "84111c1ccdb75a2da0f284572b66e854", + "aefProfiles": [ + { + "aefId": "dc76679f940d56c86987f0ba7973df0f" + } + ] + } +] diff --git a/catalogue-enhanced-test/jsonfiles/tosca_meta.json b/catalogue-enhanced-test/jsonfiles/tosca_meta.json new file mode 100644 index 0000000..55bf24a --- /dev/null +++ b/catalogue-enhanced-test/jsonfiles/tosca_meta.json @@ -0,0 +1,11 @@ +[ + "TOSCA-Meta-File-Version: 1.0", + "CSAR-Version: 1.1", + "Created-By: Ericsson Software Technology", + "Entry-Definitions: Definitions/example_cnf_vnfd.yaml", + "ETSI-Entry-Manifest: example_cnf.mf", + "ETSI-Entry-Change-Log: Artifacts/ChangeLog.txt", + "ETSI-Entry-Tests: Artifacts/Tests/", + "ETSI-Entry-Licenses: Artifacts/Licenses", + "ETSI-Entry-Certificate: example_cnf.cert" +] diff --git a/catalogue-enhanced/.gitignore b/catalogue-enhanced/.gitignore new file mode 100644 index 0000000..00f2c95 --- /dev/null +++ b/catalogue-enhanced/.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/catalogue-enhanced/Dockerfile b/catalogue-enhanced/Dockerfile new file mode 100644 index 0000000..390dcc5 --- /dev/null +++ b/catalogue-enhanced/Dockerfile @@ -0,0 +1,48 @@ +# ============LICENSE_START=============================================== +# Copyright (C) 2022 Nordix Foundation. All rights reserved. +# ======================================================================== +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END================================================= +# + +FROM python:3.8-slim-buster + +RUN pip install connexion[swagger-ui] + +#install nginx and curl +RUN apt-get update && apt-get install -y nginx=1.14.* nginx-extras curl + +WORKDIR /usr/src/app + +COPY api api +COPY nginx.conf nginx.conf +COPY certificate /usr/src/app/cert +COPY src src +COPY csar csar + +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/catalogue-enhanced/README.md b/catalogue-enhanced/README.md new file mode 100644 index 0000000..655d164 --- /dev/null +++ b/catalogue-enhanced/README.md @@ -0,0 +1,96 @@ +## 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. + +# 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): + +| 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 | +|certificate |A self-signed certificate and a key | + +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 section below outlines the supported open api 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. + +| 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. + +# Supported operations in Non-RT RIC rAPP Catalogue Enhanced + +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/ | + + +# Admin functions + +| Function | Path and parameters | +| --------------------- | ------------------- | +| 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. + +Note that test can be performed both using the nonsecure http port and the secure https port. + +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. + +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 new file mode 100644 index 0000000..262bcce --- /dev/null +++ b/catalogue-enhanced/api/rapp-catalogue-enhanced.yaml @@ -0,0 +1,292 @@ +# ============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================================================= +# + +openapi: 3.0.0 +info: + title: 'Rapp Catalogue API Enhanced' + description: 'The Nonrtric Rapp Catalogue defines API specifications to register/unregister/query generic attibutes of Rapp' + version: 1.0.0 +servers: + - url: / +paths: + '/rappcatalogue': + get: + operationId: catalogue_manager.query_all_rapp_ids + description: 'Query for all rapp identifiers' + tags: + - All rapp identifiers + responses: + 200: + description: 'Array of all rapp identifiers' + content: + application/json: + schema: + type: array + items: + "$ref": "#/components/schemas/RappId" + minItems: 0 + 429: + "$ref": "#/components/responses/429-TooManyRequests" + 503: + "$ref": "#/components/responses/503-ServiceUnavailable" + + '/rappcatalogue/{rappid}': + parameters: + - name: rappid + in: path + required: true + schema: + "$ref": "#/components/schemas/RappId" + get: + operationId: catalogue_manager.query_rapp_by_id + description: 'Query for individual rapp definition' + tags: + - Individual rapp object + responses: + 200: + description: 'The rapp definition schemas' + content: + application/json: + schema: + "$ref": "#/components/schemas/RappObject" + 404: + "$ref": "#/components/responses/404-NotFound" + 429: + "$ref": "#/components/responses/429-TooManyRequests" + 503: + "$ref": "#/components/responses/503-ServiceUnavailable" + + put: + operationId: catalogue_manager.register_rapp + description: 'Register, or update, a rapp definition' + tags: + - Individual rapp object + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/RappObject" + responses: + 200: + description: 'The rapp has been updated' + content: + application/json: + schema: + "$ref": "#/components/schemas/RappObject" + 201: + description: 'The rapp has been registered' + content: + application/json: + schema: + "$ref": "#/components/schemas/RappObject" + 400: + "$ref": "#/components/responses/400-BadRequest" + 408: + "$ref": "#/components/responses/408-RequestTimeout" + 429: + "$ref": "#/components/responses/429-TooManyRequests" + 503: + "$ref": "#/components/responses/503-ServiceUnavailable" + 507: + "$ref": "#/components/responses/507-InsufficientStorage" + + delete: + operationId: catalogue_manager.unregister_rapp + description: 'Unregister a rapp from catalogue' + tags: + - Individual rapp object + responses: + 204: + description: 'The rapp definition has been deleted' + 404: + "$ref": "#/components/responses/404-NotFound" + 408: + "$ref": "#/components/responses/408-RequestTimeout" + 429: + "$ref": "#/components/responses/429-TooManyRequests" + 503: + "$ref": "#/components/responses/503-ServiceUnavailable" + + '/rappcatalogue/{rappid}/{servicetype}': + parameters: + - name: rappid + in: path + required: true + schema: + "$ref": "#/components/schemas/RappId" + parameters: + - name: servicetype + in: path + required: true + schema: + "$ref": "#/components/schemas/ServiceType" + get: + operationId: catalogue_manager.query_api_list_by_rapp_id_and_service_type + description: 'Query for all api list by rapp_id and service_type' + tags: + - All rapp services + responses: + 200: + description: 'Array of all services' + content: + application/json: + schema: + type: array + items: + "$ref": "#/components/schemas/ServiceTypeObject" + minItems: 0 + 429: + "$ref": "#/components/responses/429-TooManyRequests" + 503: + "$ref": "#/components/responses/503-ServiceUnavailable" + + '/rappcatalogue/csar/{rappid}/toscameta': + parameters: + - name: rappid + in: path + required: true + schema: + "$ref": "#/components/schemas/RappId" + + get: + operationId: catalogue_manager.query_tosca_meta_content_by_rapp_id + description: 'Query TOSCA.meta file content by rapp_id' + tags: + - TOSCA.meta file content + responses: + 200: + description: 'TOSCA.meta details' + content: + application/json: + schema: + type: array + items: + "$ref": "#/components/schemas/ToscaMeta" + minItems: 0 + 400: + "$ref": "#/components/responses/400-BadRequest" + 404: + "$ref": "#/components/responses/404-NotFound" + 429: + "$ref": "#/components/responses/429-TooManyRequests" + 503: + "$ref": "#/components/responses/503-ServiceUnavailable" + +components: + schemas: + + RappObject: + description: 'A definition of a rapp, i.e. the schemas for a rapp that is being validated' + type: object + properties: + rappSchema: + "$ref": "#/components/schemas/JsonSchema" + required: + - rappSchema + + 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 + + JsonSchema: + description: 'A JSON schema' + type: object + + RappId: + description: 'Rapp identifier assigned when a rapp is registered' + type: string + + ServiceType: + description: 'Service identifier differantiate whether service is consumed or produced' + type: string + + ServiceTypeObject: + description: 'A JSON schema' + type: object + + ToscaMeta: + description: 'TOSCA.meta file content' + type: string + + responses: + 400-BadRequest: + description: 'Object in payload 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" + + 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/catalogue-enhanced/certificate/cert.crt b/catalogue-enhanced/certificate/cert.crt new file mode 100644 index 0000000..ecdf912 --- /dev/null +++ b/catalogue-enhanced/certificate/cert.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIUORRVFAWtFYuRZ9oXUPVpLioZiSQwDQYJKoZIhvcNAQEL +BQAwgY0xCzAJBgNVBAYTAklFMREwDwYDVQQIDAhMRUlOU1RFUjESMBAGA1UEBwwJ +V0VTVE1FQVRIMREwDwYDVQQKDAhFcmljc3NvbjEMMAoGA1UECwwDRVNUMREwDwYD +VQQDDAhlc3QudGVjaDEjMCEGCSqGSIb3DQEJARYUaGFsaWwuY2FrYWxAZXN0LnRl +Y2gwIBcNMjIxMDI2MTA1NjUzWhgPMjA1MDAzMTIxMDU2NTNaMIGNMQswCQYDVQQG +EwJJRTERMA8GA1UECAwITEVJTlNURVIxEjAQBgNVBAcMCVdFU1RNRUFUSDERMA8G +A1UECgwIRXJpY3Nzb24xDDAKBgNVBAsMA0VTVDERMA8GA1UEAwwIZXN0LnRlY2gx +IzAhBgkqhkiG9w0BCQEWFGhhbGlsLmNha2FsQGVzdC50ZWNoMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnI+W+Vmzft6ZOOl/ZoImiz8CRsmJbeKhD6f8 +pXoGJC0GbGj3eltvxdYpFAWM57uDfZvbcKPDq4A8jxmBxJniwIAhXaDWoZR3Brz7 +jO0hIsO+vAyDtbhg07EJo1KDf5k68ijF7818lufYcJGZlmsDb11GliAFpCzoKoYi +ULiZX8UrBybSfFAnTr5M+Akb/zfQWRLTN+YTjirILpBNLHccZOEv+9yMaj0Rl7S8 +r15JQgHuLK1mO9pdN7CQ2wvkCmEZ6WlJeQzENvRtQ5iChbiOwC0YQkjKklW90Y7L +jS7QFkQ95icme0BUC5iX0yukYiqUPfbLI0/pEKUr6ePB6Xf0dQIDAQABo1MwUTAd +BgNVHQ4EFgQU9DqxWd9hVVaOaN8QxWNqBgIA5HwwHwYDVR0jBBgwFoAU9DqxWd9h +VVaOaN8QxWNqBgIA5HwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEAN5J62EhG5Fj0dufcYfwmFYzbRZgiAFLhLiedKGvcU8V1TZrvnDpcjKm31/W3 +3SN+l5IKfQFgW99BO+A6ccqELR7fEt/Uj2yKqpR+cvErkvgdobbbuQAQLduEclyO +ZrToOavj8gD3S0Zpi+uT3Ftb2FB/fBy/C0RtO8Oaypq0pHPhe2p/KQ3PNDBqs2ES +P2A36hePvhkKxxc+6Sjfx1Sb8Z3cw57SRYGj7wBM0EVzgaTii/13hyFO1d7QBzWs +8EDtxLp4xhq0flWSjAjYN8GTm3xZdChSCGLW2/GD2a3aE2EDfx0VWTWUZQrtQJka +Ze6wYYxKurcS1KcrG02QcJcaOw== +-----END CERTIFICATE----- diff --git a/catalogue-enhanced/certificate/generate_cert_and_key.sh b/catalogue-enhanced/certificate/generate_cert_and_key.sh new file mode 100755 index 0000000..b6468df --- /dev/null +++ b/catalogue-enhanced/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=LEINSTER/L=WESTMEATH/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/catalogue-enhanced/certificate/key.crt b/catalogue-enhanced/certificate/key.crt new file mode 100644 index 0000000..c5976cf --- /dev/null +++ b/catalogue-enhanced/certificate/key.crt @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIYZlCXDfs28gCAggA +MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECFDy0yPJFfJeBIIEyMvq3yk4M8kp +C/mGRY9AEVg49ZOOXmfscfDLfDNm2fF9gtZl9wxRMDjKK5tlAxBfZqw1a5YZfor0 +4S4np6Kq5Rqdq78WVOZlG1a75u6TJ/oSYu6IGT75u5MeypCsWkraCmiO3Ps5Gy3r +TZcG4s6EoREMT1+yK+WQRfO/AxEPaUT1Xfd1fFwH7nl3cjbJnDsLLhvL2ufvy4im +JM8JkeeASPWBurl4a61Mg6voO9v4Ai5k7+Cl1+4OvTLbU9RrBAj4wQx667fg1WBV +hkaelBW7cEn6THK/KkufDmXKyoafkVFb12ErYhyrw/afB0utVYfJrH8Em5HBEys+ +nMkCA5Kcy61ZSugnM12CZcke+uoGOjvkcxwsbMGup4vMmP8E8pIcoFh26KeHU3zz +V+mI2+PY1SEYc/lYnrU4/Fx7tD1zqq+5iVyhuisy4CgzwH8NTNu8umjC/m2WhLyy +HoYWyV5qA7JE+e/8+8ss11BMCwh55n2JcwsYj+pWFD3xUwd29CbW0MZyYbX3p4Sr +i5PReKZ4rmuzq4EPth+cn8q3Uc4gbyS/nXBd0NOHxg2X9f5IuNz83VJcSSZh7UIA +HmWn3+sxDODO8yLiztT9+UMreW2fA0RAOxD6Ku9e3ByZiNlRDZqWyz8XelZoJPh1 +Y7sUcSi30GsKABq882A+eSsCPDUGs9AVH1/7/BThXq5Kvjn8djtYp2duTl9cOz3F +Cp33WBL1yE7IBnq3NlYs/C9bJPK5n/uc3Hi0zNWuHrn/AZ222pBuPI+HHA2ooFNM +LiUehvz0/Xi118hjuxczlgl7zSHjKZrgqB4dM9571q8sA3T/7sfqULmA8rp/xZlM +1uCDVg3gGmglRWJFr4W4jBfea5W9TLZLc/R98iIMNTY1MLPD9pMdjih0CxcVUAsC +cDf4lMzaOuluE+snlUIoiDEIdMZ+1aCSYqyVIq2BEyMIMs7hiOEI7zTYGLMVM8wg +3wtkwVEZ/fNbs2AtpmiOgr+MXa4a1Rjte7FQrqO2hKnNdLyo5d4r1CfbI5ycrJo0 +8Cc7paF12EQ+piMU1XrHew93BkCeeE3t4/fyuFWtKk0SmgqEZHizIxv6H+OrHZfb +80mvPqA+4do/3EdPx/zKHBP3LYsp2l6ttXo/ji95SAqDPEL0tylD0WAZDzw7pWhA +v0OoTqg7GQxOqaa2Zty8x2X4nROwqnnu5ZMPg8SA4kxD9kslervOIfUftcDvxgm+ +BEx3RSh+IwXaVfnH4DLNWtQ1OGrSXUUbBZ6wH2zducWggcmbqUHl2xlFAr6/rL2Q +jr1yPkUooCOSdr8OEYjpiawCZWBH6iMtdFErKppL969b3OcTP4om+vhl/wEWwQ9h +sgfefcXeejpwOKldHMZVgFMA7PNUy+3vMWz6eL+2zUN3Hy5mLaXnmQXLEGvt9NWJ +2yugYgR2+L0UhvXBF/xpITc1q5TLKNAbEuIBfj/kP/2+/tfSzN1TPFKMtbVZ0hzW +xwiv9haAyMRHW5ds9Iu03F3CglHz6Tfax3qyBXkX1Y3Ont5MWabivFkO/kkY9tLg +Bnx9UPgv5zJVTkUR25Zafe5wKRCEPSkdLtF3v6n3YBC67IUxZo9hNfMsuV/YApvv +Uqa1+3WrkRiTghW3LL/V6g== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/catalogue-enhanced/certificate/pass b/catalogue-enhanced/certificate/pass new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/catalogue-enhanced/certificate/pass @@ -0,0 +1 @@ +test diff --git a/catalogue-enhanced/csar/rapp1/rapp1.csar b/catalogue-enhanced/csar/rapp1/rapp1.csar new file mode 100755 index 0000000000000000000000000000000000000000..54c3be2503f47ed784a0d905e7d089fcac787a76 GIT binary patch literal 54766 zcmb5V19Yrgm;M{uww>(Qwr$(CZQI$gZQHhYY$rQ*a`!p?@B7{J-M-zoYm8ZM)flyY zHS3u**SqR@YspIigP;Ha06+kAD&{DR9g9~>wJs)UA7o7SM z&_$;gpdt!0!yhc~BT{b{VqBC9KoXm2)?OWnWtC18iD8-({3&3WF;A7Mojo%nIatQ{ z`$hG3$TW%M!y;UOrp}=iX7*uQTc(}c&nhjWy}sRrN2Y}67gxtw_1kxI)#j9qw~zDe zJ#C(RU&0^c(?@L<)X@3a&fs8MbNH^Bb@U9bh971JrU+(VfYzF(P1CTuUnh08h5vPje7FpUWcaT>mUbZSVaw^J0oQ`3P2Rd&nQB`vZ6Ir zeXvu4QpQKW?02CH9dVpmZ9idu(%!iF{U{myxX|h&M^JiYd0Fn#+v-N-=G-Xr2KRe< zJ$M{`J-n;;#4A}Wmemt&T}j3PVw0uB9Aq6jRt%6YdnF*uqb6m9l&4`W2?ou}Fx7$~ z?0CR^OM335V|m2S=8M%EL09ufw2_U0y%3S7hUGZI(Huu(83l>a@6&D&BAenhQ!>13 zG1u)0^^5-W3!sBrBXIY1h8gaAurvB zMI)qu)Hihi-em5|!of>1Y@39DudM$&WE4_k&PqmKDW0VMyjrOSP~uTTn1z!yFqx3) zq9BT)4t_ZQoRMYO!lCrwma|8=5($|<5ROVD3n>Wkf;+e(DBho)NRru-Kn8@kIKX0B z)eIna_qgE+3_!&ziAi69H_RA84l6B{!cz=l+R8pst>zbUWs7*t zFN_5EOW>ZN(|TbIJ(w_ApTLk;dCrG<`6pNHx<6@;v@i12Z5BzSv8oD` z0fbl$b@l7N-|lBY3e#|9Smr^5=af;1=EyS;Ml3N%MeG7Y5V}(#HM6q!s`w0wClbWI z0p$PmG}ND-1^^gU&QbXL!T-5~|I^dTa!Nu1KV(dt4U7$(4e0)E>Z6=XJ>1;?S5yB5 z`j5$h-N#U6(_}5X=MP935C8!4uj4;Ke?O(Q`NLI-?}r+ohY7v$iR|HL>waQA0TDpl z{7qF{!cQ6bkw09(NR6ap%KODZ_rjg zU1WkhH*zY`vVA)V5|6va_1gcOWF9_P)Uq2Nuf%PcflAsn{erP;Ij)m?Xgh2?BEYQE zspfQN4uLDY!l)EAe$jRxIwdCUGJ5B6j(GpM zm5U|_K{&KGHPe=L-xG?kkDmA(#xK_KgLQrn+xUMIqx8jS0mhSP$XgYxc3%?dYgVc0 zSX`_#F$tJ(+?ePJKD94+3y_H5GH#vXe9gu%hVG|pR>q}l(LKh=>yt#wuaR9T?A8eK zz9Br)+u};?M->;9W(F29F|lSn`Hgy=P@7|2q9^e=pe#IxghJx!`Z@#ZaGLOGur(s6 zOr^nmFfYY9L2#nub|`ywgmWyo=pnKawBL&`$N2BbMlZN?AYrj$DvTbP$g&kiv&K3> z>6u)x?QKp4SRMJ>@+bWyX!xwdOMUjc1-+3khAPp~PRvz{^m-SDVVcc&JDaN`D>BKo_DS7uYNHHNYH`h|Iz?v&WEs1K(gekWw69YWQe zC#d*Z;2!-IHe!YuqBVX#`e5B8g7nv|4-` zu}db@6kM3@hX`bZO#>R};l34Lto!Om+Dm~SPs7LoOozuD5PCD6arsNJnc_W?vn{|T z<<+i*@QanRnT_R-xvnjMT%ysgOza(swwuIlje%>~P+&wa&glUC^+@XaJ0-=J2exDJf^?zBdDP1Fjg00TF3A`0mpW3}y0x|uqeKa$F@XoU z&tpA*8+Uv^GV|ZXGht5dWSyEciX-H$rq3663#4~H@@`g&8r-UZRVzS~vr?eTdjl+O zP}9JM<2ScfEw`3aT%WjUT+CoPu??R-z0{Pe0Rb)-o(m@pL=DTCh6o|V975S(;$@RR zkQSKs%uegWx=orR<*Y3f%iuN? zL-KY}9-iVuiB4nDI`*;vb;9nf2yvOD38FM)v(hv9Z8&U_#R+wX9U)yb9Z+ba5f1|Q zyi0wUjUarZ(N5>P=X|r%}|94|~vvTbnofq56^5g>oU^5yoYe;X^bC@{VUg~P*` zUlEtTVvsx@6GNVZbcAFy=zJ&@@sv$@g_#F}QUn5F9`+1ibwprcfFHY#nx`nfAE=kh33^2Ae>J8mOPgbJim3O%^Wfbq0{ zHTtPWdDqyvYP|6MMCLtbGRsLs2iW;fh*f8}86zleY?q$=&h+o zx>}T!wU?vMF`|=;FkvSs0WEtiI5|GltJqa^c$}cv`+2|YHYc)^0sMn zx!z@YJE#xjs1IBLJeK*exKa~zT|80CplUIy~wAvax+;@DU<<=L6`CTN<%#~e%yd+nZvO9OQ7XrCP|kl| z=J+&X;Um~nKe1JuLIwFcUloCzHZ7U(+zS0eUw!+R%uV^dZD(z4@0UO4c!V1FP2)Epw{4c zOdY$Ys_{B1d(8VqW@TEA>9w2pQlm#i*5ilIPF7iV!$-fq_Nt;7AM*7Ajj$-lj3 zM+u%BHo>Zq7ky%sK)k1xhqP1gH}3WusY=rQE_FRv;B7BMUwMiOy|nb?Mo#YHChO(` ze`W^jB!&rb#DbUO2hNjNpEx{NepaNo6KMP(ao1UF0s>pnno#@Ggx?)@>}7Sdm*I|a zXW}&F36uE&BIwzgbHvL=UA-yo{&<%RW0}ae=sQt&*>ggklY9|w;`?FdOathk!-M|T zBnyvWpjrL37$eVhym1&8E%N%>f$UNvOSwc*z<%=qBk8^6+OTRcuqy0gn7n=c%}nG| z=J+KuaYn33#}vpwwYO?R;nZ4bjtk18 zM9yI7guKntf`lW@{Rsxg8Bpi)=W`^JKs=6_33wcp?r8^n0c&dPIkzHguYx%xAG~a9 zc}62%S-`{JUoXi0ae-fG_`4%&Z1Ym%69u~sVCpua`79aEBT34D@!bnkGj|MRwdI2< zRtxPnu)aW~p{-%IQ1&H3Y$DIDb2$&8;58z(TT;C=KJr*FV%>3kL2%DWz;4A3H^;t9 z7BOK#?-#Nc6-i&I;|AqS0qR~aRGYEzT45GuL(3Fm5zAjXZ-_$bZT*S7rH5B>9a0V} zTOG!lY*3Xv4>vdO%4hAdB3}|8DeE;KqD$RgTq2>D33H0g8rM6JIntkuchdV{-^GEw z*cb@|wiLW;-mC|5Tm_#9^UpU4XdWLFpHPH9A2EygOb-u2@cG^js`W)A(L>KC8{e24 zTAHRr&)V2~zp!k&N8y!jhzBGB?4=CnPtw;5K=G}3SFbIcHk8 zd!`YUYr_LhHUs06Pb2LZRBgJi}42a%xUd2ucWPHcc6@eGt_O znBh;!5QmdL1Nd51)dP>Th&03WKNZwb=u1UQ@lL15F=Wx4!9>}U4uSf~`%E#$%cDP( zKtnTix6ImN-lCN3n^1p-cDAdv*wTd4NRenVzD4S-x6dSDDDD-siy$6fR3#1?|_@7rO_{kT9f4F^)F1*z#>Q0srUNobJ5`6nr#eo;^e6CSWxy*6{Kd? zjM&dz;kd@}b4mlc;U~K_Rd-!a&de*gk=pTScOUIBzfupRr(PFj4NkWq3Eq7``c_lM zPwc_Kj-|8Ga~{O(nN!$RqH51hvKK?7l6>XZuy=BosB_I$2SGL@MxdVo-tFaQo*PWv zYKoV3=^XcKX4Y8mEd%}<8;eFZ!H%l=T}bSeY9=`{=9pvw6|i4lP%F|eh)Apgfy;|* z)%0jnq(KMa39GADss2ULR)(TTpH6_!1{i-a*64u--8!PvWfsgTw`sK6gCHRb-HYb- z;fsrT?@f?g5+n`*y;J5uvQ$-~=(Q-^P}67UEzI7>m9iOB(AJWQnZn5aJN}kDV?Z&1 zWNeQDDs&>L$Xy2_R*H!=uv>M3XH2fxkLJ#14XsG=2<|KlNm5OfxY08*?#v}ecCKkx zmCV!eMh@khP=FC?t9)|Q0KVar|=p$Y$a8Gx3~-A z{wFpHvtic9l@v}hkQ1)oq|2u-^vEC@jI8JncI4OSwE{6xg{j}4gWh;Qn&TB!8zC|H zMc5%w!C!M=LaGtcPZWRYo>sd8H#Mq1__J}a&-h>-9$|v2=0Ff{@u=~YUr;`)s;}m< z9ej^sVSsdCzY&&?u~@$0W>D>?o0M|G9SttJ11c#o5sEOzC4<+~=EG@irq+#pR@o7x{w`%2-u+)Y$%g}z~$`j!a66><3QSza%d9S4ykp6kCW z3X1o`AG488cq#Wk3cu!SxV zJw=^d@-AA(hG%=?L=mU+!hh1c{*+E|o})M-MwEgD5j-IiBsk$JOfpS5cYTJ#Mh*J; zLa%efJ&`*r8I`*9Qst;0111>yp8P&~826$smwWA29QQ)8$o(m2B5!c7#CY<3`J!eF z4kJ36ox?nM=CAqrE}%G@a6`p#g!0lQ+TCezo){Yx^=z3=D8}#zgXtmxfsD1=PKfxs zgzYV4qFbIdWd+`KSZz^#hcfb8E4)159`OEwxqxz35l+dL`#C%{SMb9;eZFbzrc5?= z`^kX2jh2_@rwn+uinAK_e zbvA>-bG~}PZOLd_e4S%{)V2)~4!1372yf-KS5ZVl_OReJ2)T7=a}}Fo+$ZV#=d8IT zghcyrt=qay%-8dovC^mGgHJLOJ0{N%NAqE&V(fPly88)+MHjP73kPSFyvjIhvKQYJ z>#S?K*cZ^h{I;=^tljB_l@*`~S3?aQ|GL`YZm;82T6M-@QBH|C05$ zd8g-UVC`b^KRZDGTln85-v0*vA2$47nIEZo)nKpz0F?ZRf4YwUslwlIT4yuQ|IV`t z@iI}@mRTgr+`VQ+;O4i7xP}YhuLJ1c+QOu1BXh6Y4M5mL26J=E1#wFgFr7&|@o;V9+4+# z#j|)-aYVi|rnx5OQTA-)qE&IXqT;7gfY9A~T->0*FF_!^5fd!sJBKwQK!b!4m57W% zD}|S^*x?|uqfi&}51i@zZp>miikv9UCKLz5_@9wNI$5mol)`KvU@7M?Phw5#V5*7< zTXs{6=O>zojd%b6E^9!JS@H+EF2cg%gI!SwJw_~!PiPmlT9^=2qZ`BldxT$%^LZTQ zpU3_X-LWwTss6XyF@nTmd+HSgm$(dV(vO@$Kitgdy>%9~fcJYH)-L86B;Q9HT6^+} zc?<;S0D9n@8D3}F0TT<-XPgMjz-J{RRA28iwAsD}y$=j_UR?d*$% z=jk+J5_lii#M1aeg10~;uz`ZgY2|9@oIrf~pdupL>Lk6&jfEfcneEyxlCvL5&h1c> z7=vtzf$9eWV#P{*@Oc5~`LwsTB79E^d2AT`vGe)|&8CI3DhIH2WX>ZeCa}c;YTs|! zhPu|1dv~omMx4{SiI*1otn6eG@?L>kx~iwbLt4e|Lk4_1D1JG=Bv=9L%c#F08}nYn z#FrcPfG3p*1N?vrKLnzi2R4ET1LqbD{dHa_=h498$+XmiS&PerpGgacmh=R>4duan z3cg8r=cX_IKsMBbGeqwlLdC!&AEME5@E)}~C-s3F!y4mEn!gWpKm$gmaYjEr@QXL-m4LwvxoA7* zof=BkgeSRsV;D5^tGsM9E7x$gbNK3jlq5MH6KK^cCJF~XmgN=Y>(UGiQEuq4DW9w+ z7hKX1(087)tnH9zlUTnpfJt7i;kDIx#T6b!z;Fm~OQCZLKg$AUw$TA0k3M-Fz6iz) z#whT8SZ5GYJt?8PB+V(9G`YW%47)lQo{--%R`KJZo}N8_HN(|N>&d@WmRBq@p$1dF zJ?jII`1IaxSYIF}hHgiP_zUGo^Yi%G>@N0s!kd=x0VkHT<^qgI(Dy6t9**MV#ofjE zs(e@Kr7KG(Ljir`2j!GzM6Wgr*2k?nca2mVjdw&{G$4T`D{fB;n=!=tzN5M%oH10vNbUrxI*mL!H3lXk;eF;hUx5y z>P-B?ZBHiTw+T0S(U~~BuBKoT#S9%V#pQdah*Ht+KKlgda+TO?3NmWtw{+&K@ZG3o z^i01Z&bxe;{xNCp`&MRQ(&GJIUPvafSa0F19Anshh^8|V9tgxbqNpK?F%TmcIP@~p zxcqM8g8tGH8A%fYQtF48dqS*OXeyT1==a=Xw1`^bSrb12U#cPpSq$9m88IQFs&1oLB^`JcM8XQr!n(L3P=8nXg6=QTz1d}jHZ6qayo>c!+mT;1yy?2 z5k7W@sD+o!ot?+BVNKreCx>5A(13_ zc@GBjP3k;S?JHkFCPu&ON<1{WD(pKV&hBj8uWaC>xI|0+sOCA}CXJ-u%F3JUBsLb} znl-O6^T;c_@r)Zt^-?Amf>`0U*}tLd?^BLSHx0<{B|*MRZ=)= zWce%>ZhgbM_w{J@%~#Fy%^9)lFN;>Ay7S>9xr@i#UObBXh~M34?Jo-_BeA`HY%e<87U9U3BuZx^n4JJH#)S{CI z9XIf6!5JT{5>3f?}P=mcGvxIt((4m)0y z{M(+B?p%`ckOYndTT{U}9^1Hz10IQy8I=?@q?PRuu3s*V9$zVWHOI^ViF}gX8KZR< zDey%NwTDxQ3JmmdQ{%;{(U>?&&0wioC85beiZeX(2@`pR4ETBP$wW z+j@p=iy1xOi{T}Mx5DU@HndfO*H{pQfF&E~LX(oJEEx#Rw_R#m7vTw-m*q%#x{RxXjP4pi9JnlQw zv_vi{8i@T;$ZxfDs>)cpepK&VKFgEW8dk$-4020K@2#!Cjto@bj>9div3AUemKtyL zqE+j&Wy|i>_*7Bgw0p!3+`r1aG8$I{wbK1^d=#?Rc(43q}SiEC#4BIsF`as|_ z@VZaj7?5Cy=O?&G!>m%F+{FdYAihB@BHzjIdk&0p^I}HNnS?07-M2V&$*BI_Hpvu) zE%}1jOweY>hB47UyjbKl23V5fci)CUqzt**q|7iJ{Kw7#jX0c;wZ1B+dSZpJ&>!cl z-J$;r{%)*X{jgO73|@QqR>@l+%0nuLv%4ku0A-vpEN0)EcXUS|kP25cP9&EklTYT< znbM`k&yr1q(xIS-o48e+-*4P0=QO&pWGlb{Zv-(>hh@D$8aHT*+)PxKpV|2x8=m>& zJfi<_biYxccxJtNNqY`{9H+hvjnDWBX6VlR1Pra60VK}3%lgBmfC0i5;o@n&(hVZHhE1>m8xnXpa z4zKkZ-Gw~SoEe+In-Ek!a9e>eN=|AmHs{VgL3074mItx0me;aA@aqgR4r2);@8zLx zO)O5Q1!cx}2eW%KRy;V)vT8H%d|||-Muq(GWWZW47W8ED>X*Sl-Neg8&@OA_-uMnU zN^cHf-Q&C-@%G=kQBYM2T;|Y&P5CtGB|4Zpf7WkNDe(e5L=Qw=v|G>>>pbjfc1?(0 zYd1b8*kE)MaUy`d_1UZuk-5SWsnwaaj>MWHRu;xf&>7gzQP) z2Sv0d%FY6Jkw+%v(-gs~0aFfzsewUPXHD1$ldJZ!0nIAU1UeIAl`IQ2KGy{VX<0h? zu!3OIL&k)5eA&ext9pU??EKp?zoZYncOgB974n_@IroV&$HR;d9CA*#O~qq~GF%+J zJl>sCQvEA~{N9T2Tt=n4sJ~?kq&`&?)?Ll;I_OXIQ_EKE?xrq#X`Ly`NcP@*o!%Y% zIoYpMGZ$EVXsBi6Q=B9W#FPdWEXaGirn=Y0Vknpwc(OTMcpfXJ@2-fVunwwo-xj_2 z_Uq=8)M7^=R>555T!lJbWRz`>DZo1l8=h7hk2jwt23!Gn%PJ#gWH~b!1f7Sq=d6kX zLwkV^DW-m0j)}uKT9sOoHJ);GGJ2A1+$10$=hHr(VaXtKAc=X_ImJpChDsbjgXTOT zyHkW5UYq^)=WvTy(_~pPNX4}}|Ej?B^SpOHXAP2{aC;BFHj%%MPK7YLk+2P=DzJl& z_)_Bt=c{scgh@yeXch7e^ii8GW`3BWCywbjBrz z%h2dyoM2pHl0dWb_MMLJY>Y@>{7+6-kq@ z59Qc&pMCjG+s>mLqO$0gVYOb-eI|XSiFxo;7W@1+y*`U>4Zb8njFu0ShNJJ~i!JF8 z4y&`sJ!0JG1=_cE(cKBImU6?(p_Z{k$4<`8$L#jL|HpmQ85r;mVgL+zds6G}RQ>qQ zGb1=1Lhf7gL>c0+uYOAHIOKQ2SRlpsu=mFxy9W!WQ`&kTi>1yRG*Blu`{txD$APL_ z8E-EgPY+Uhm!eb$v7W|)7pNKDCszpN3f)-e&}9bfV#dAl&>}fUx#<=-D`|Oj+2KX^ z)IwMX@;zSsd~2u5t%Pj|_T)psCp@~J!!J!mPwh_&lf26Jq+X`*TnFrGCvZ-Or>`wc!2WXoSpO++X71H9Sa$FU;~ z31?ov7Ia$_hv4S-lha%x${3%N)4T=3xZ~~nx4I7W)9FO$UvO3{_so*kj->ll?#ZHD z;vmfo9564(n6MMW$v6@?cB{Xyb^<^$E!oV)Al57I>Yzau;GkyDXB$Om2mNF^fWh|1$$v_?gKZm$V(&+>sGO%kTADjj~Uo4)F{iXa2FeGU)0 zp%e=BCQJW;J}MP|rlUvt^HcOPyB6~TG_lU(vBh3I$FqHPb^ZW7rYP~V!?+=feb_|O zk!g=M3smNJS!KFx)Q@e}dF`WVceZrL8x1Yikia1hjI8lttfP33e7uV+$59qkers7es18^H4$_O^UC1 zR!F5p0ut^GHEXBKbTt{G0_*s%55K%`@5EZVw@BP;FQ3;l&93S}jU{Z#>eUnP&S{2}vY;S{aw!dCe?C}dVL?)mBt6O0 zG%CiMrk?a7Jduo4EX85DMx~OKUp=X?~Hw+6^ahu*sei~b` zH{3L+hXWUHOhqMPDhw~C$ZlWebhcwYXPH+RJ@6J zGU8j`Qty{bQeS#HduuS^8{|C~DuMXWOT7%{XnsLI<#E_9@Nk_Kyc~R#u>p_Wxf7xk z2i<{Iue=0qX5AesErI^*mJW0jdCnH5nHAuTzO;EiR?_((vw2-Y)nm|YaG0ZW&BJ zF(j)+-1x;27FO45CRByuilNl2O}BIXCMmM@Yn;eKeRti~5q%T+OpYMV&%)pYeyRC}wj0CelzhuQ+`%qr& zTSrZxC{$6CyA-fNyMimNPen*Z4v=IYl7Ve(uv5~c6KYn4=Rl-(BX3rGQb7&}1Ak&h zU6=o)q5d>6&OLD_Ofw70zw9=g%HzFze~sJog~r^wstZJ&H|qA{*gUKPFHB^AoR7Gl z61{b(?W-I^Z8K=d#iMEyS}UV){C620&Td-6pFd>r#0ew4k=$it-kod1h#e@_fwJni zsP_rHkI14+*YY43Q!*tO8$!QBp28FyR+nAv zW`Q^DXBm#4v#ZKXGp2H&>K|)sauv0L&0Y^%j5u{&P#MvG%{^W%KC;;rhLg}+d1x*v z+Z$FTc?kUDLTvR06kx}3g}RQnVT^RG^#-z|=7jU8&YbycWZ6dIu5AkH$l5j}HEpdH zs@HOh6wml|UwhU~a>;h+<(bNp$=T$O%5}HOJzB^0d6yR5OsNd6Yk=$_n7QF599=U2u`5fJ-$72P`k%@aKI}0%T|3}Q;g=J zoa$86u)Ck* zM}m))o( z;F>{RCJO9uRB7{p4vlZe@X#Lo+|22oy&+x5d$|3AfyV41b0s#39=Aywxi?=S&@>kq2?bBI` zE>(s!uEYTRK{NQ#M`fU?_{o>klSN1sHYKx@o-JBYX)VWL=EC6{yse*;d^Gk~YEk6( z+A^hu(*#o(h@R|Yt!J}oCq6$OYDm#}ZF)`1Jc9N^v8(MhA}Q?`wW;rxt-IEZc1ijJ z^Y*VkYgY#F5H3f0^YxYlOjD`QgX+!aLH_$GGU>GwwjkoYu6LX_AtAlv*@%v7gv-sV z21x-e&fDfYc7_GPEj>d{NkAmQ&4GTj*Dyler7gZD>&$WCt&mOipWK@$)E9-m|rUI?pDy&x5UDl7za{0p`P>I@PmCeu|~Dbgv<#Ea$9VwV-3r6YpL(otl3 zS-=P%G}gs4?et$<3)_c>jKqfab7X^-a(~mij|88fhqLz8M`v@>1sX>QFy2~>`Jiq# zlsLJygV<>vnc@UT>*9x`(lo9J8jR$>+s;Qtj=s^Lv#0YbDJquBxG0w*#wZSq+P(7{ z*Cj7(5+EYC0>RgzyvawK|DKGebC|hws<9jB$mN44Q-|q}dHqdW^8r(mL{oz}*3WPRR;19xZ<;r_s zdlIV15|kaFLhi!nZd<6(T2P}tV^-^%zv`~n+nvQZE}ju&P}$J`Y_mN%pY^+_^nrI!Rs2jepZf-r zaB-R_>jZ70MY-|nMbV9?sYZlDIi3m7FWNG%7V4Bp#WZeL_E|&7Rqk>Y*ylRt$*Ph% zm+qovY8q4Kj6Az)X^mQl>Ws+E1Ge2?dnM-P#=HfqpLQ@shq)vtuhcM6X=CK!>LHk<|0|KZzV@8oZ;E{KOYp#fBz;p;DTio9i<-=`Lpqna1^}RUU<9ufi~jnuy6ax zbF_a`mqv6i3A*^a#Ip&hj4MBP6101Nvs7d;;jz5lrKcvzv#qaQ1kU?}$A#d``8VjD zlTDo|2839O9m9cmGPg<@@N_NV_)5}ZX+;@(dEZB9d0nWFZqc)-q<3wa&+T+|PKTZB zm>}#^x*)PKB=GX!_(bRr>-yWyccMDxF*`-jNq4iRBnt1#l+0N=s$MKirJHoDB7}SU zpzLrVBBnCt&vp5?Gj>G=VU^MxzDHL@U!?bNH|l_^7lhE$dB4Qz3m^J|#v{^`XBY?8 z^d|ffvaFp{mc|pnL~x`6XP23K8#ecrXK2M`ihJlHaNiKk?NC1KZWii8Z;4v##Cx$# zmx$aAPW#DM>BH6ACMhT(fKHaj&{Y7t_5yI?V52a~gZ1@KNZEqf~ z_clo{ykA`3vkLvLk7LQopPfvMuG`j;w|cf&kwb}h!ngWNY&beeF%*%@nvyyO+`RL9 zKaMZHAYOb>&ut#vYjI&XpDqfpeW*QK{mKerGg!0sr#0*_ho8(dKKOSUby17Sm<$;# zcihBxXp!FCN)9cyL2=ZL!$x%^$;9us6zVRDsGWIG%J;{5)J-D_ys=U`BAM&XM0puu z<-Nlkn9^vlYLO8!+Vd(3h6k0e27CKj#WC>36v<56k692M4rrW z5lXor6wycx>fC*N#+Z)e0t`}HkaJ&YONM+lN&4k5(hlX?dk1#sR$Uq$svVFvt%q=Y z@8OvO@Q#SCfX+P5G89z^af-I#fAVlc#xnT)oJ~_;GUV5o;DEW1Ry`x;dNB^3sDSyU zUj9p{^VfvH`12TEu{2#e~-J7p#1{3Kk$L zj{26nFEEX{;+k6{<&(~q(%i(e#u)!fP#!yjmrs`#l&Vt#;amjQBrwzx1*ag14OPaj z=&l^8EvoE3`tcP+;GrPd_oa~WGlxf2qc}LUOMY;v1BLGhi$gzGA4wmI{}uHzs_nxU z%bVI+`@;?TXbw0<>{N;gId50ym8h&fh^LVHO&E=kMd(;f1OU`k77t<{FF>@u_o3-0 z6)0hfw2B}6)3;ltoo0HizZ=C0mwpI7XeB>KE1w`OOz1AHd(}GF%1-zra3;LpD5#qn zG0p%*+!c;`h{v+cT2ehTj%}AD&+VBh-(_#@lLKZ{MS72)SE2YNl2lvMN2!6kP1vOh zu}NMTZCYc4D#&bchnS20aFSP57#3t>s(Pda#9+)!O(t48=jmQqiA6|5{@`fOasSLf zQemJ0;9~k(=_z=oI;8Gfg2Cg!8@k}j(Y+44Mz-6MhI8D_FAFh60H|pcw&3X+XY{G& zfhz~0^mDOd`!J2`ghcVgekt_ouE>ie8%RFa|G2PUOjd zMViK0RZ-!H#x>j@^3-5N?bOs&{pcMJ)Vr_I^-31t*(YVXk;<#f&xgPb;R|3VY_K@D zeJ3xd&)O5E(@$i_MJ=bb$`ZxKcYhE`h+LA}l7XyFicMamfK<79?8Ze(oO9In=`V#c2F&3jRGv56l#{Zew&_tpnj z2?jA~azWWVDA)duoC(N$UsSK|)Wy24^sTxk@YROM4PoslC^&Khdkbyza(~^lb(ylg z+S^%37l|hWLz;Av>cN046K{xO+2|pc8}{BtfA_R0-6FlmaCM9x$tS#|MHH?dS~DR3 zO2U#An{-AypFi#t-577w)0gCUjBk2%-tSPHTR$H^pR0%9bwJsbuXlyqEQxLnCE3~gz1y;%_kO8T zsg}vdOHs>!BP7>-A)`b;Vhk%pR+HuT-Y>x1`@}h8m>ZJ$Fp9asX8lu^@ggb0H@5bn z)gtWMz1}tBFelB$toEi|VAFL7fMM|5^U=*(6A`DwHsZX4w79boJ-1yS;Hr%!ny3is zQJ@Y@0OoF)jM7)1~=gCC7ZuK%KDdGPFqpE>lC&GV~NRUh;JizwF6uO^Su$4R*Wz zlBsx=y{KNJv==+EsjeMc(Qp?RwmA@0Fsk!}S&Hbp1I_gn7>;ei(e97cS@r^MeDC_2 zlSpt~`kUZ_aN(YjQ{g;jzrcSG4;PsAH(NVC{L*lWZO><53?gIs9T^Jh<@Yg4o3Miu zGy&Db9hg$I^8@~8&;Ggl2;OyE_oP8ud6`?gQ~aKbU&k2Nud|LzFqsKP@u6jXy9ExQo{ADa67v44r&r9gzm3zftK1zNfc{c;TZ4h_V8hA>(nh@>ja)$1x_OCK?ie`L#G%s33A@_{dOF#j=Wb`?(knFl4>iv5Q0?hn|Io z@fPbhp)_+jJ&x!^lm_9WC5WeXddsF`Z&|FUr;L{Uxn5QR&}$Sxg-wBXfHgBfjjF$o zgMZK%T@ITZf?KjaL^9YHk*KOUCQ8>s(!ey`P|D0R@JHdeT$J5pc=P)E=?5RN3??gW z)%Wpvr1IAjo|&En*wBluQ?vis^j>JIK3F+AlH#;6q}85Y)YHb$HZJ})%uR_)7S)AX zQa^rE3jCaQTY%UHcd=zzOHlcaRy$|8xwti)&TWK^vsC1uCznRSf%H8`mv->LEQ4)U zd1(){@$` zJNZjltN=)3Mk4GTDrmI&*r1osq$4b?gj%e{qR#Jj>0PvfufL zgXzh_hyT&Py@GMNA^4ceDAw(YzB6F1^? zpL-*^SHyf-4>=?AEoYAL4Q^>o0l&rNavA@lG5~o?K+y3#26KG!;22hJyRB5S31_6U z?j`M$Kk{&a6@cj9Sy7hqRmckX_=S7trbysw*0M9*BC?>YyMGnJ*kKw`j>pR&x@14c6Aeji7qHg;C zX#b@b@c3pz*Cj5D+S!!b?fMwRlDpB4*GB^6(@Od9_R>UBzZ0*%IDoZQ*_Lxxm2f07 zSlUr!!u&Vs`2z!p_(AOl!kBm_6q&k@go6@}7KLc354Wg`r&4c&byDc1hDBMZ*Xlru zgJ`PACbLmS%Eo8CT%RvA|AeCM|5B0KV300E5yRuoe;)t7*4qfBTgPOc&?0C$bjkEj z#ft`|$D*8kkhNQC#sN8y{{2YdF!(rjSLw(5X`vHj4yDc5Ab^Pmu*G^`#BYDlQx1N06 z=VFc?pv`L=t1^D3}6<*s^Au@7X&h0y!3Uc5waraDx{vBVWm${<=@*gJVgJA}{*MOqf1j0D4pz(UA>q~X;jVH^E<q0g&Q0qz2fvu*LLTBRV$gI}#Upa6dO`z^0Y&!*`k$Ua&AD!(!f-!&FZi@s0U zcYdExuQ4{4U!J13pudK!$;+;+O{zTsoXp4A^st;H+aqrJNqe!$z0*?KWbcHvB`n=p zX>VCJ`Pmua2$I|=A#-jXDBOn0zk}utou-%B||XHeoj@(HvEPa#$V#D-=;A1X15RapG30Y z*44Y?dCd%;d5RW=?bCBW70l!GI=zI1U<35U*Trb5)~yfv&VvVcMo=5~be1@~TK6zt zYZ=Q20z&t;>_c4bSZr;PK)!O-=q#2hM|{b0;j=@1G6}lEsnKpMKE)z3xc;2Kgq+`S+Kw+fie-l^<}JX=n;KSl2=vjz9a^O#Kp2+st*i0 zn1(vGT0+FpgZ4MgrL1-xKXEyFu9U`YHs8t(aTDxF$b+lW?P5ij_)=e3nWtiEJRQES zrT+#;t2c}ky&zIVO%SV=9Nye|3bMO!Dp~S#KS^-i1)UfVhq#L7#~8wn2Q)Q}(Z02Y z3hY-;qxCnLchOeBMfA*GyPKoNoM=-fa19yWx!uNVcYl@g&c&mrrJk)idBL*O;UGx# z988n$)l-Y?D~qkq5D*YA^ea?#F_Lx{K=XPyS*i6-Yb;06p_}6YVzXsV;OF>OJy$f9 zS}dy!$&(}l;msR{+i{&!R{gBU8?W&ab4&M_<+McK{uYw0mdnZw99D|+pUKLb(YdaQ z$VWmy!Y1?=h&=B4Ci_3EyMX{JC zXj4gEF8)y`Y~A9Cemmo3{s4?yewVm#w_o4w`zv{$#e&uO^h3#Xa)>`o)B`28VX{7; z@+UNRaw9`wy2AdbDWa+(q;eB(tZa*;6-p_$=b_w~MG~k84p}5F`KA~0UPksu-T3a4LRi-=w(Q$TDPxu)DQvm0upp+NGu*p5KSX8`?T>L(E|4loFE_Nt|{;UYfRQL?w z0i1AGzZ_oWA)iJHH8uqqlLD~WYD>U+%x@T5dcUpVi@m{@$_J9i`f-=c&6V9)5TI{_ zm2y=)O#zPQKt<|(uq>o;#3Uh8w5h)?E?qsweAX4+$X5ET6cbW`j(C~{x@Ix=;XV<5 z&O?V(JtsYD@tDQ9MKJNG0jcVdbm(PXT)@)WA2ZwedgUCAt5cp)Y3$d{ktV_y07eWd zVHDPJ-@r8rdE&BG7yP5;9!S*^Ib3rv^%zr8xr6-D6spnti$1`iXDtl(fONAN2a7+iDJi+r^36S>25bGc&%}^hD#!Y8K#qX|%JjdHFazT% zn4bZaYma?`rUD`H8~Ui3jywWMt+)5{k$42mwtL0oH?AyuS1~292+=k3F836= z#*F%|+1TY%grXRbZk41%?3v^V99RJcQ-G=a_Y zNINEY-Z)~-Hb0FYI(n%R1p4NC-f7I5Ofi#W0}Qtri6ct;5JW^&U@!3P9OoA8jamL5 zkE-EXgS+emM+Yy8Yt7v;1>TlH!it8{D=SfYVmE?5ct^_@Dc6%U9;IG&g>5KqHz)T! zM>X54AW+^1s9p&N-m}D)dZyx^$i`5gBBv-aqZh6BAGZwi^=GqcB{IbH9jB_eKz%aj zn4=e#*fnM?`44epf?d1V7w3l7E!QcZps6iceV?3I95E8IZ=oJIJzZ}}RviT|8B`>R zu)9(%U@!}}H0Ib7S9pcUFBAhd;8MsMJHS1a)*!fVNy7Y&szjv5SdQV-?quqnaL-QI zLmV;U;pwwoW+Ax_B5?zQ9%o|K5onB1$KvIcnUBOvB|1`C%=(8QHq)9KwAx_qv7&S_ z`k+2Q?N@-rTYVIyI&sK%`bLKGLF>JjHl@fL>PW2GGN3cZ>Yx5oWT*cf}nm4cAX z2;Z>+Tzn?O6q-5?XA}-rNu_MYI9FIgA*f=C^S%hMGQOf^)kZKk=t=3~d#I5spLGF# z(4T23d{Kvp81w{QzHRjcu(14lhtK-rjHTpFU{^VO2wx+71ApRV<2NFxnvsTgv^f<6 zKI6>F!TLi2tJ>@OKmJVw1HR&cd=1pD0k;NuAV;D-Q=oKE(72jOK*9D!F8jG2DDMCt zCm@(PA=a3|^f}&mb#8`6#qmKxI@S>&D5L(6-}$J2;ugEF+Ds(Iox^n7DZgqY1nAXq zFM-j}Z8IbU{3bbAgOJ?pY73Ec1sv|e<3jPI{{^!uUxP!CvL!5w2NeX!ARj??xDN8p z112;=?!WN*VJp}y@yyP?qvPB6z$_^k>gsiak4Jj{X8df6n;od{nWZY9{k6&6ktrPU zvq9}N@Pw$FOQNn*zLRQ3&KQCa{E=w-Ti%y%nD5ii751x3Kc^JeoNo1ugWpTJk6rzU z_4Bf*Z2JWsxwvtz_9uTxKGkERu@95^`KNx@_AX3>v0haRr&+>%rIn6C*n9dZ3->z< zn@XpmEg~aD`OI{1p5a#?cKy>CAoH-H2b2;8^xXfy;CcUTwu9vu^vWUu0BHMH|NmyY z|FQ}GpUifuo~8=Rx=(Zv66r4N64Jpg#M}QqR|vrK8!*HKyW%dm+;BTVrOw&gkDbil zSo+V&te)2?FS9(2Xd*Po(p;iB2(TL-iGg4-HdH6F!@u!;|K@{Od>A8`mO*NkBW<9; zJ~tVu!cU;KRB;JuwWFWhg!UBAG@#?9mGT_tqDZ6BqEotd- zXZl*jmO)L;5*cxeCN(a!Jy%aQ)YlcFhXRP^j$vwp&iMquXwlst-+G#J`1|eoA+?>uhj9yt-E{vnrVN^gf&2PU$iC);d5} z>=!iZwDgLQk1p0EK#^@8;102vrKoK@F0>z^(fGq`_J zBB0$DKXC@>zunIuEG~SlfaTO=MSyS0(l)j1qBT5V-Wfo z?!zF*DJpGbabBcJ{M^c$dNdQIkA8Q&F3PszK)1`Uk+=4Jy->k@bMdr8l z2D1G+yym#;(u``~o~>5&LCn1-3Qcj~L9Nli z1L8_DoS;Mw3A(BR?En_&GedI>S(^iNuv#lnxKN)V+}Q^fK+XI60QIm&is0e>cnih8 zBPLKv0YP>2@dG9;I~Xv`9c)+J@6MJO3;3-eMkGG46P&~G^t_yu0e8?M3DCQJ|NWk# z+2|BMU6;3g#k!J~yU2KV`K7f(x}@?*n|<)ofs>QnAZHg9zxvJcz7Mqo1o9eN`^UFg_ER7EZ1rN z_-!`i_^JXwnS)*va*K$Hrbm5gdd9;|5d6TeugX$Dv{ifK7PctkcZZ%QxHErQ_1)5^ z%qR0t=6?ieNAbWuaQ(XKhMCMJb}abuK4e)6t9CbToI=_ouvEHL!p?EFT(z*cxw(mH z=zyoc5F{{l8B+wc2-PA;0SZx$A*#euS$I`bX(xId=55jqohgdqy|6_`=E(*&Exqzr zb>W~#S;pAQ54;wBIOF4!h3Dub$FC`a6q^$0(SKXeK)+x+QkZTmvScyptW8$6H~W zQ7@uZV*D1Za}Z;Mc(3=8!vyK>AR_xdM6Muvu(LYk;b=LdFWHx|0|tUAGj{OR@|XSHjpqG`f|(ubSlLs6zb))wI23$k~i zj5QbH-c+GMM%SKsE9P-|XvPt2*@~L{L?gFhX>ciQXyt_vDvAd*BC3uUr0ngYWJ=QAx(Jbu4XL(4SA=@qE2(UYG&@yo8R1*@S#Q*>LMejzF@GFb>P;} z`My#yEqSV@d{%w8W_ulZPd}WILkVv*Zs5wp-n!63BgiNCJG|ic6l~9=f$VvIR^vVE zkjIh*pON3wq5?j}ev(A4nGPcSqr=Vbo8U>~+}|+nw*vvPp-5GO>T(9TgkD{Ov~X0&q8)X^B451*s3@{KZ3jD9AVs&O;s3>Vq76#66yysKVz_ebgqzLbtG8A}zJ zgm!!$s)md^x7yen&`%E{5}+1#dq>g&D4f5l`XL3{Y(U=417AMFKqaJ36oTZ0-o zf7;P1I@J6(-5`0g?PPZwFlml7RVW5Hy^hYNv)AygblxDM*#;bc1}x(Oi?>fY;D85K z+EGEdH{q5sOzou6@UwN5GglKWwV8YsW;%iC>oq?A#c8a1Gse#S&3H&m-OMb1x88H* z!I9mF(p%u$!8BH9q6I%R%Nub9Nyg(uqTVOM0Zhs`G%Z5C`lzAubAgJc+B`zYYdPm$ zNnOmo(@;UWRSMJ?rKIfen}+XG)d@rhHjP6+a=6lM(mqzG_pjW3bW~Z`krjA_q8nirTLkapP z$iXLu_h{VIX*H;p(4D4nk!K+MG2i89s;wBEs)SEQd}{(0Xu1@R{BqkD+-By7e!cj5 z#`!1{;(0@4&|qEP_{hxh5kjbS3(>DvX%=pOhe*5RdFa-sI}zD71QMuK&F4#}iL=I_XCCpx!uHky-VEPYg`E51B>E=oKIthI z_s=_Grcs_pFW_s-DvSB#0&ukjw$$?n+YJ>zx!_a_(~REXSy}+M%r` zi1sP<%YBZ;&#gwh?)1l^oE^-KX&ROC2a~AmIKz-J4bp67^49>-W;Ial`hmW`XfDd7 z%{*FRMk)HJ><|-~t=9Q!_f|lSjaH`=)3D;ZN)My|t<1Zej)hhGkuSE;Q099^=B}?g z?6LCcnOUTWof~aup0d5X7MWzO(exDm>O@Mb*gU?q49bEHO`e(QVIH$b3}g0RKD0WX z?wQPOYM6J+m8{i`g|7zypH~Qh+ETdwt+RA zsB+9XJW);cr=!w*Imn8io~ezXhj*-Z`_HmQpeP|W8dm#Jd65|#oh&_V{I&pR*VA)n zaP)W1lP+CqnXbQF-JTq64b&WcwEE~goi1QMgoOk2;R*xuX>cjWTHe~!K^N^m=&Z?P zYh+K1T9Pogi87SNIvR4)B-SM8>3KJ$w-WOh^gpdV6lSkd$xRF8AepZ zFAu3Um0bhz25YAeDuO)<7i#mfpB_;ja4s~(MT7J$bOHUKSs?B#OJDYY5|ug|BA-|Y z?hYZwvGX@9IP(4=>B+%}4to%O`02BTS|nyE$X!%*s}xrQu|f?O}71t<8AC zw97h+kmcOp3VfUre{7Y;XWJxsG~0=AnVFd9V4u%+cPY0#VKh%K%0(R2Mk66OZep+# zT0v@oPMP9hKbeb-Pq2Ix2VX&`?(+74OdP7aYG2sDhlX5nNP)~AwYNI7_*zOPeK=3U zT5(93JOr13NLQZ|F%Da(Clo4H;6QbyX#^g``&d z%8N6Is5y}F!u$Q)XIV(I44_?dajgpO%cI@L@o1~r32WB(>hT=6la}no z^CV7k0k;s@f9;bigu4cl2k+eh#RvJ#jq zdcF>bBrgbrU9%|X0~&O5Vi!oEBl-a@a9sR^wJ=(m29?1{2-F*s?C<*x19CiLo&TWq z0nVdFoYV9;)2&vT01n$-V`O~JQ(?)orb32$^+uO0o!b4;bVe&U&Xely+C%s;t+*;s z4}(JZNl(!Ek(Tf)D=3Yz126(jpcjrBNvS!1_v_$9|*GUjS^(zPuT@{aWRBSfti)GhXn)o01~X#-cLwM3?o6K#4g zWmxeV7Qa~E@_~jSUfd^s&+$B>bRoSo4bq(g zZ#Q~2bGU&Rh@A$gg~h6QUwtfrd;J96be-1uW@4J<+{_Y|hWL{_ku$n~S(t>kQD!FG zaunc!IM23%3ZYN1LPwZmtmSXUWxpN%&wkuKOG{AMq;yy)_1?tg@m9qk-?6Pxgvcb+ za#9i)441B{!a6kKEiWA;_K?po7*EWAP|c+Za{mvsIPd-YSeQkkkJm$U(>;yv~`wLodOGl4lcuJ}GUX4X3)~S1e`MP}#T7AvRLPSoxu8Cpw*; zBIG+_l0LK_T^e+N1%>Nm=jk*o;^#{ZD0>zxKL#PNxFzOL?BEy$+3B z=xe(}3*YUG&NjK4yu-3?32p5z`&bO^1^aBU$ILyM&_U&+=F9=vpa?1RyV5>%(-aFl z@&(h3M)O8S3Ni2tVWHhznYVD0%g;c`D}{7j9;H^ed~n+!$o*kcR$sqz!aw!S_ehWq=9>58&y{@2p;)EN4;l(6PyY~#5-F!BZsgO}1r zEo?^Rgfdw*v|J*Wew`)ibRrB*Fp3hj7x%&3C9v(E-yWatz6#(CTcT1Cnj_nJt`}A~ zYIjY4spoxoM^>u3rJHiMxbpBCi^>7CAHOgq!{1I*ai{e)zw2$eNuBFBrI-`O( zxUBx!sxct+k5+B5W1}AH zz_m>7k%{EV9Buh87T^xBH@$^A493!6%@72ZE+<>}0P;bTVnS>+$yn|Lr>O^Dm39h=k9MI(9T z9`+=!`j!}<-wR$d>4uk_@ZeO$h%m16SCe5=5JFkJhE`wTEO*U<92s9Eh6-M>9R@bX zwkCXKqBt7Q|2yZqb)M>lvD#Q8j1I*Z&-^_RrinR=yvTfCT+G(a-?j-W<`)Ii3e%Qo z7$~l;%IKmBH?7{dv#Nx+uo7Uszq_B7(xjuKQKe{XYX4yNb&C}XzYcyKa3^o-IFtn6 zSEubZ|Cw`{e^Ha7VaDD!c++_&^Q|X{9Zru%O66nzP^#y6^ONxyhF!x9kVWVMYu)gIrAo%|0`O|%Fg1c0 z{^~P7lifdkusP>0(&}XmjIyPUsB#K(dH7T3b)Y|eW?I~IO>};NjPP-l^ zqCEZIy>0Df_6QVm%XgmH0cY8bv;40U&!u%jHH#gn|BlZvML%|4W%V0%&acYI5; zAt@!_TDAB#w^BZPn5LJG+(H2P>V|}ITKe!7b>GXJg7t~|SbVl_osWC&*1}}>1N9bz z@nJC2u1Q!5Ja5?U?QvCLk{q_jW+c66QR{_$#kUH_y#B;2&!jae+w#W56;OV@%1Qy2 z?OkC1PreRLt@>6?HV1cKx?ZN-HEaQ@wmYO&o8>=N%i=ehIQhy_BKR50GVvSL#591r z@@&X2j>Hsagvr}9DZ2{*$2Zd%UaNi>cV>rvWgIJP!XfBba|5+cHl8#yd`4#G#pbVvr94lh5jES9ii+sJbH+U>$jtx`6yLBp+HNF%0`?JUTm%@N zC@hhq=X{8j!>w@q(c`QR4xYfkj|a z1Kgf>{Q5KB4kHCD^KfJkR{(JE26^!Wayf}tM1H|7%tJVuK0De;iMH>G!;GHWTZ_*%NlL^oIP-Wvn_Wd z2FmJ9dpQA{l26$sAbry#vmrVf1|4tFUrn~bXp=aLPA)xrdY~uphSw78Y5#D?5`$4m2 zdwDT0_&Z%j?UF1FsOWjR_0>sWq;c;7BKs(_*VXniEd>0eAQn%Yy63%mBGmafky^DK z^z|81=s?AO!@|$~;jrFtE8`bcab5@^wq($w54{iNz}70=cvqN9*=(hJr3Hhxod|f& zYkv1=L!G^$DE6rT+_lo=ImuW3)ZlB?QCq~?8l!k|bk92vVvmDK@xzo4aWJQlhB^4M zpUg_dC`FvX!!YZJ;H?zB;%!p_J6%B|M8ipBRr^x-%}5!{C`iM@DYlc@eJkE z9~RK(l|bm0oV<*Imd`mc-DWS5WyMNqiicC5;H9^2g|emnI^q5?aZ|HxYqaji0erUm z?mhz~ciS8qxY)~fnYLutZ}wce+J(WY;8+b%?)Zj|1D+={p}uY>Nb<+KF8z0CR-V0Q zgZ8>Ho0FB5Stb<{8ZOSJdY(2(C-35fY#vpv znc*H=BXw9gc_kQ*SdcmNVjpL6Fx!pLzPg7C+V-Ro*j@a+$Io#KOAkN!cqJT*7lW^R zcLSAS_jH7THZJK+`MEY4Zsb051wq^I~zw7yx`iYiXAP^N1_4eULZJ%|!+ zsH#Nhx}&`N2-+r4GzGb-1RkXCOn`6JucD*hBjJy1_A^^cKrF;3$OWswn>tHq7{kQs zR;2~8Mt^xaPTqLmZ;{B=JM0rm#~#rwTcflJ4>|@6Z>roD+}5So=wB)gh(AQ|5_^|b zr)b#t<6iJ_-s#$Puby${Bq2ufn+tOjy4TX{dbLO4? z5ujqDpkhyT+Dhjx0vhl>5Azeb=%I;E*_Yx@kGZjZV6WxB4~`xR2^n2px*6Dg{Loyh zHw`Cu3qS4GBz&EItBZIg&H3wJ z)37NurQAuVcBK?{gC+c>Qz}2CC=ZGX9I8H)&OQc|?I1!)Y~+UJ^s&c6f@ef4#z_?% zfzgmtppw*na_spGY?**&OC=@&{*uX>8Yid&sP}O6x=83@0}ptBY57s*j@7Be5Xg4# z4*C(clciawCa*Qu*n5GE+}*yNmqApcKEFr`S%9qhdb3;xN32zER^tsuf-&&&R}dB8 zx_Y??!z2hzOu&~!^$`HEtJ>0_1h&WvUc;2<^4K^142~7U%a^$f1x(FsC$1LoyHW+P zAPoKA`+2UY&384UA?&~;LtexDNH|@^v(7V;9V&@0*XfBvxRP?-Sts*rp zWj1+!hMr$%0<|3*@xWiPanhFPZmm$YP^KI0Cxi3BL3-8$LsIuhdXdIb%S=DalWa|X zBuGD}I8@h)AV}>wD0w-QFQaH{P-TwRFe{#Kzg`o|Ha`vTcaJQbYF71allI~`+$D)g zK3G1@u4kEIOUUw{c0mv0fSSBWa~sleuD5!hmd=1q!~OuAgI&ZR zOuZ3%x{+7%>N-oouB;YSqV1W&7~o#jsY1(X4r?&KAUewQA>7kz)u+vzirKpHGDpaS z=_h=!oSY+^A0V~u)Zp6(I(Q$Y>b=gZ1?H(8ZpHTBLraJQinR_ngG>okZ$Gr=F7+uk zlvf734vf#yU6)Abjox_PB3Dj1z-cUHbYtFxqJt*dUX1bPR1eix1%=RdEF)HH<`did z0CrEDlWJm#wf}atbl#zOD21Y5T+Jll^hfhbQNH;{RuunY(3t#htad~ zz7r8ntzP-{}lTFMnxC4Ftjr_m9aOc zbMbKbFRu81qA&kb{{yqG;%Z7DYv@Ekz)HZ#!NtVM#lS|u#K6eOMIh?oVDIE&YC_;* zPw-!1q5n%HU;V%4_^%xOfd8^Z{}Yo5`LFr^9*)`duhoC*f3&;Kn6(-dKnV9oc9uY# z!1LZhKO11zu=S$hJp+CtM+0Fjc*)b&?`qn{Z~P}yQM&M9@CnS#BuU?v0r-2=g$W2J z5Nh(ZM}t}rm-I(?98!SC0^}2h~Ztcar9hu-DumkfhN49ak&7-9hqgeSlq8vXNw6pOgK7t|BZ`&>@&pp#h|>j zu&@e8>NYD~0)Yh9ahBmUfmd zmiBi4Z${?KzayOe{~qIiR{3w$8~qqgcPcCZK*m4BJnjEj#ni>wQs2(ZP2btxhJk@m z-`L*P*4|Fv#na*c0+WBOr5TUif#}D+_bcFm?-rYy%6TBW*A_V@2Uc%x&Wn6XsKHWh z*E%fYo-EzSTI8Q!C-Fk+YAr{MZD6aB=7CZbXd=g=aUGTja8A0|G0ng5R>cwH9_b9GYkg@r&&3nq&j8WTwg7oCfa!A z;m5=8^InJ&XTEjsPOr~!0~J12jFnhy!!lqD_h1F3~?y z(3DDC3o+B6SAe8O`hK!>wt1rA_;Y?nG8t3<(!3ul2t~X~B<)l*;m+OEOL3g#$UBnr zfe%eFPy>>zS}QYN5uNBZ_w;_mduT|DRlFv|U{|A~MpTrKQ&~-w_`#_)KJia|Dvn(e z>c|u&=r5H=mZ*Ugky7Yn%l`OzA4i))1ya{}0yvqjRm=pgN#;CjRQ#7KN(RhxuDYSZ zi4gjnP3Cy7;6QqR?2gKKqL|t+7n11I74=9VOcg39=UB$eNR0#OWn#$)Ej!}pyBxOT zteEMbO$#g0ut{Lm+SaKrKHsvBK1ORY#$|QWs#l@KIR8xy!)*$x0Zp_>MUyr&RHft& z$;!^)1CDI6eI-=py%1plW=CJ&sz`=2UXDah_k&mulMGb5V?j=(7G~Ir$NiQSQeAHdQWT#nwfef9TUE5 z6oQLgu{JCWK}=PPT3CpdjgP(HNlrgTu9T{wx0xiEAhFNh#lcEOGberZ85cwF3udsb zyvFEGJ!|_UlFtf*_lteG+4m`?@6WKGt(cl?2Y@t{JlDwe8BFttQU+v+uUH<{B~ucS zV0}~a{<|a+ZEQen#&8V>8ehRM3tn~}FY5d)0ynt8K`U`R?J1=XD`0He%`Tzhf zXrjegHwB*cDf;{rwkA6;w)`u$=WYqU(P57b-{nZq%wBX68=pz8t4mZb^195GVD@kx zeACuB)yO@u38!Bt&F7c5N{X)@|M8$KkOr}$t}ER=1^*g}risl#7uqj_&a(LG%a4~MtBH~wdOD~#Zi(w|kqk|u-_C>LNy zVIs6X_QJxZsc5cZ14g9XJs26_KkES?3!@+qRcjVXY@;yTR!>6)0hVUTZyuB2^9DRh za^mp~Z}$Y>Zi=7<|1KU|glFpT(c9W?0wEeC&A9Zd$O+f;0R;eSnSxzF`6cz#N#;n` zLlQq^kP1=sJ>TAVbua)O=BP+&gfi!b^F-AXdj+}f2)Euiiv}Z<7(>0p4+T(Pq55h9 zhU>X!%jPSoiqB3ZiQnE^aSGOMGtBS78T6+$C};0?C&}tOjGQ)o584Ci{hZQPG94$D zJah&t|tw>Af zi0l6{;~PTvQFEh4Q{M~_xaJN_&=z4~y!dFOkO+4xRf6Jco)C?g?i!#in&2qC&JVQ$ z>?i!Mk>JX&k1qANaz?9Qp*5MNg5AOTDJOCNemXA~t|2sJ>C@(H@yU+-pc(ViVDK?L z+=3c-Km1zE6CYIh?oml_B*JRy4_m@;L;>8wPW7sBX3U!2>mK?UJywq+n zV<*6bMr)+DUv^8u!?cyQrqU-d+zJP)3wVKLJ@;SABueIVq0(I5R}&Qhw2Gr9CYFZZjPM;$L!(Ye@xdzw~wl-XfcTnU$1wjIHS^?s5JfiLlQ7Q$R_ zEXgk+Khu>b$vNj_97`w=$lb^buAooPr=SjaR*VIc{3E5PGDN@`+))=wh!fliII|lE z|C;-faKd|afxBqOZ6T!@-w0}NM8<;Ko*d7N8a}b)kAOx0B@H;3yD56X#q4_BR<3iZZrVi0t_#Qn5J0>^={lBvaTmk} z5ff>XCPI>j)k*)pV(b%kbA0)=fqDItD>y&$Y72Clx@47AN!#}%kVDnR+K2T5RcOEM z=Cps2@D*J{;JDwIna!)jhu+iMXCtGK#g{6nE~=rWB3oPzNZu;fX;xk2o!4j8YwS$% z-y2j2;BiDb8U2xoRGaH-HE*oFBSRCu1ti~*_!@g=q>HGwraIM_iyMNo>0)he7(`@3 zTX_nyfyq(`h@dw5ziqmWkT|-$iQVS%!x%EW>X{gjSmgbKn%y%HYZNGG!L_X`M!5MH z;JBkjjIz#W)jo*HGp+Y&gH8Luu*+C#O1BXPbtsqccR2yG>GU5p+*%#cCwMh5WqDAv zl~X@D*YM>CHJp***gJS*(ED~F5#Yhqesx0wl_u?hwU_V>BdF6c4t+r1=c&W!!Eo0Lac zQK9^Yd@i%-{$?s~m-;^%dN0E`A?(p|q2=oHUBCCYUX%myL%W`;(fdZ?k@#9)QU@j2 zYe8@-?RMo)-y%9SE_}p7#cksJ80f!VCk3gL#Y6Fc=2<+^+){e;m^Po96re{T%ZhZO1}U87f6S z+UIl~qWg9QHoKtCN9LSO?c7@bdoxW_fyirip zuV!`_J4|67(K~0-)YizfQPYp^1?6>@)i?_e-yH8A>8jfO z>3kbXX^kMa()Az2jmQFm;=;28*VA+j9cA0Sw81>_{)$LyjBX>!t6MULT=I6D7V!6= zw@ZSPo0I>X3s5UjYZ@$D;&oEpMD>?vWNiI&_6>#3SB++svDsB=uG0 z;QkA%s(W=`twcsHm-GU_s#@hvLcXgjYs%JIH4QS8)@#FOD5I&JSGPE0xcrATwHQ4~ zXQ{IgyqU_nOr9(r8tT37%LUUPh5hyWW@m|sfc0dhcEHLxA{-N3Z$+OxxhQ-mL)~qu z9WTncN;PV+X-Joy?M3~TTSE5j{xL?dpvY>uYEFK(Uk)}f8uiA<;Vbo6v9!*1K2^Zb zRM1ty`5&O?9mXUXP;mPqZXH{}DMy7iEB%B??y5Is9@h@~)S@iqc6oqki};j!GF)^_ zkJOfJFt^^a0d#?iE6sixJDTjt+J8Httt}sSj_u;+CX-(U4w5QXPF{;aZYA-rQ=0lu zhgRtBn*hDY2O0XBz8L>kH0hV-60AAYL7Nbdt&!PdbhMr+5td&&YXtvMGMI+?Y44b| zZLB2Ry$)FJA83Cb0dJX9%MRtmcr8s#s|iRdYlyAA5=Q}{lO1Hm|kV?zMD!{SuCYqj=5Y0w>g~-4G#yM4@ngsxt@GZJ+sNkrCFd!M7HTzQCyF zNmLs~Y=3kA+O^3x6Pyso0LNAFg_VHdv-Z=)il!S0|fVYlog{WQVOd)6-c z`suar|7yPrl{}@qGWWDlcuoL^t&=gQL1Af6}x= z`p^)CereC_?&I_J#0d*+0#M{p8{IuwHA8KluwguV=RoEo4S-ay)u`&OSSM!COtnzU zG-YJ$f}Qf5eTePp>EW`-upro&`SiFNBQVGY#oS{AM)2YD;^!> zMu4ZXqVSkIsgVn{dcsZef_sKg8qhz+o=}xX;(tL19VT!FMh;T(AJq+GM^i44Hr^7}iESGfx9S ztXFlhmL)|Z10vQ#ZKF*p28IzIIc$;2%xLp+RAND5@3E&a;1jv~kzDcm6YH_O8Y)5E zGES{<&Fw2@o4k;L8-P=IhZ$W4&@Ww+N~o$s2!Yju)PS2*X{oZZL-BU>13Z01Ice9F zP&3GU#~sIndlNw@q+zu8_ctcGa@r(nOI1g-YenaU3eE^Au~{oJTHrKp$hJY2<&v2r zvKvvpaFb>{szXb|(@f>+%m~?oBdlOkQEyGk<}MwIW36%NX8U%0B(zVCJRB%Rb6HJ21=|LbLsEa3(Jp`X4Bo^|2*He59Ii zm?y;`d}>VU!MW2wnLYC49KStLT2N0sT(;KvROT*tZW-T?BXh{DDN@)@McS3{>-t!l zAg$zM?@Cp?Lq%&X!!@B6;(g$RzJ8cxU6Q&&n3&IY7{fNjbSc?TWyHo%DWzs9XQ!l6bL1BSS^jAGx2=vHXq2y#OpexSmr#an`E zF5I!ER17r)MR2HnAhG7lDp*@)C#3op@8!l096h?w-@;iwhoU(thV=ijQ#X+@H-&TpN~ z!?oTYBF};1;HT!AHXv3pmm446+e9~i3Y>kL2x?2dF2Q z-zw*FwWk0yKS>0oC+IO)e*|KV{OM?^AKXcHWasU*FnDyf=y4eRts1nSwf(A^pRwTZ z{;f_nP4@_V^h$WcTc~a!fNoJ;7rnv9L8!e!uZA_tS}(;QJ7$P7%rm|i_Xhbeewo0C z3{CxKKaZEQ#yDMkfCVp?kh1JYj<2TUZVXDsB*mHW7bOc`KMdFqd@p?jk>n!EYNQ*TGZ1@i32nYh5&ZGB;%tYD>2Yy$0*B!QcA zHdX`SQz`#ks0~uZrJDq?87NpBOp8*72lTF~M+Kn1dxL|)kq+b%C=W4rqs6fLh8DYD za9J+UXDy5nj$;5vkVoT4n_Z3w1J1KL4J(|E#8TE3ylkr=*EjiPNW#NBfX~d7q3xpu ziWwcUTN&oMFI&T&>o4W>!S2j5FKA|&zq)y52j>hJW_2mH5F{U&)jC-exJm;s6nb15 zWO3d(*C8bL=qvBAxwi~5O!NKsce87~IdZqw!|{aJjIDK8PHjS0&Yz37H`Ff@895hy zIBD}t!i3X(gV27GUbUzDPJ4iUU%$%GsT$VN)HB#fz?X}H6_1tjAgKLEG(!wXT7lC; zGU15XjQ*Of`FKvHp`9N}%g*Vo;(2U+K$_qVdYSJL!gCOY4cXdr2k2oQ5DyQl1Q;D8 zk$H~>_ACCg*;~FwxqB~jv^w~cWRm9Xft1x8k37Ldf8OaLc{5L2OK#gCJp~Qw_q-BH zRyCG?HkVIft2TP+<^Kg%;s_>4lhf4MI9Qn&Gz*)p)lxW5)}cAXbIL~I^Iy<9&FN3c zg>Y8yLxWrnp^sTgCCWX!PLI#ZjMmi&aBaRPtkyseGS$&J(Md0c9NcXp`q9(o6jUm? z9d;ob7P14?82qi}I7kyheG-eKk0bZP)tT}t;hGtQ48KhsG~(I*eNR&^(M%l$lcTFb6@f*#f6}4 zH!pilTHkH*4c{xg7EXwHHy#3Boz?C;nRWyhWxblz`_BMz=~HLU@YEsV{2R~yKTrS3 z8qGJ4m`SA)H5*P3mG@bJ+TEYKdx1-abpP0v$hNiO7>T@gwVrb(EjtwKQs~RC zNrVW%jJ+gV;mbL68bX-@{}FsU>3kveAx=%59b0O!r1lEWdu~e*W^_l-b1A)8i+4Gx zMe20S^<4DE`#BrW-_rUPs*8S9{s7->2B!EoZUc=Yjr0DOJr*#GI@^tUsZAo3YQcF9 z`Vv0|mvEDaQKT#sHKjbK86ymWoNi!@t9Jldk=d4F79MdRhcoUYe-X*FaW|4Rh#c&^C&;9ML|PrW62-vimJYv$w%Mcyd1k#GXNgG4w6&v1AdCus1o z11IrPbG&ac$kj(hH%HKT*FF5d2{TZ3I-=4Fxztyw2(QCWOzNfxmtP1Wp$MOh%M zRme-lQ|hErsVN8~ZF!!4j&gkmgrj);A(^)F`C~HPZ^OW1RG@}egUOBzx*f5pSLVo~ z=H>Kjk3~ah(p}@qgPhU~f)GD+r{*YS-i@Zaye}s1zapc_tRCZuQa#3COp2m@h3eLH z%ROnWFy)VmswD+X^wW!QV)tdK#4OLDxtI>B6;{LhcMxj$CI4YjfctF1R^48N;yu1}p{$Av&#laQKI5NQB`-;);q^;FD?vl=Yl$x-o8V-nm~_uX-8mah ze;fX($ifVv@-d4t*D}f}h4dC_@E-6php7ClCj}`umQV3UN!4nnCU^)Z8Er%OxDfYc zy*XK&_dJMN_*Ah2hQ!oe+&9H*|7-Jc$G$rie6%N$v0P;0Zf*FlDmzSKWjIH@U2|MV zGno(j{>N?;r;i`6UT1k!?TC7b&DvAZ9}6R4(6LQZrFXqEwiHQ!XYs_Vc~wjt!Pc%7 z+<;Dd_f6W1qRbb&c<5}{pN$^6D>04LeS+BJ~~PjM@Pd*s$s9N z9=0r#B|a6$d@+y2K7a!^8z-}CIdrFydR-Ho1Tka4XY+VNu+2ut-GgA{4J&h?O4Wy* zo0jsaZXJx+EWZ}wSW<^}ydvwOh~r+2iVm5f`ycx{^BRCsV5rtJ)mCE125QhEQb)S* z65Q`&u)lh|a&sY)l+J^$;%5FXA9#25xv=cX6yN-U%^WD7|sDVR#tB$){#uBr=nECEcj(yEP=geF>h^2xq#ALB73Pr|uhYKJdp z{+mb8Zni7QGx2Pg=S~FDD)_#YRmsUHmA>BF=uu#jS>|RHMw7Fwp&r?MWeJC{&O_^B zjdoqmT)*f~l{F+wnp*EB55_*}VZ$60;q5F(bBP1(QifS7jft%UxD_eI=6&%uZ@cBk zbMA{|N80JSor#?r*A_Q#!0HLC#d5|yB~)q7p;4?MtLQN=M6#S$t2p_Z$Z%kc-+1Mq z1vn}xt3l|<%shWW$=po^bw}zEx&4cY-=VYtx1oy9T3LkQ z+}EE7}wrmyE+6Y&791thHqNlC)5EZtMMU@<>UPUl9C4`uNqUvSJceoaSOCCGvav3k_>#KW3up|Nc0Rw*e`c4A#3o@nn1f>!F^OKv8!UZra;m86rF+Xq(=SM zm~8{=BveT{hJV-zeGi&pFSG9|RcAUP>Wr{l_hmP!JHJpJw^aRX_=0!ooh zvu>Uq)Znk9pFVP$L_PVDmge$;DFNlxG^!ZtiDoe`p^WnaPR#9nG0ulMAp%<^aGV&X zWTo+-SHshh!VhsoB#4rdR9n6L`tte??DwfDH}f2rCLc=WAo}=rF5XBi>a)LYbP%2P ztyTQq;HSWmsK#ULZi7se zYaFOayN#jGrj?2d-A>wDkZ$Vkre&+6dH(~Kv_o8mo9WZ4!ISlIJg%T*GR>5H> zX)P>`(um#0$PI|yxLh$mzL8RJ6R$Yvv2?X9;5xBf3GLC3Dt|Qd>?1+CglBdLPiKou zdoL=gk#Xd=4hGd|1?)d6ZHGd1lG3Lkg4DUam|kNqpjTcK*lb*doTT&)5TWQx1r+&= zB2WV#MDnLyni0tK%XZtO4L2AXJ)n8b)z#o%2DudoJ`cchcEHIQs_wG<_|4Fmf5p~E zdL+xL)>aJIbUL)X=q;z14JklJCX)Rq^%Hn7Zi5x$3mCD)e=!XUpaanRd8q{KqF}yM zYZL$en^{TP73vB4C64guEF?kdyA^5u$G}hZ2>nSinXfOWiNr6reu$gL&p&;hFn4$A z{1f$EtK7x-20UO5i(Al32}xWvEmGdL*a1G6nl43<^`J$T0dKr#L{Iw?(@;%p%eddK zmKD><-?8q_1v)j;zG}izx8cq7w5cAfs+r&TS+JF=wl!?~>r7=$IU1q-tb_Yd1DZ2A zh*3ez%^OQQyC;;#wL;&A(F9S!N6s>aTJ{BrS?>v!M-wl}6M}K?Tmst%M;4EFh{e_4 zXXyVXmxjSZgOdjwM-q5il=0Q({@tNJ--oJoo36}fUOo7w2_h+)?ILP zoYL=`p|K}wykWX%%$4wqD;;m6cibS`YH?kEJROugLgB2xoPBTq#uWnw`g^q2XNKdH z;OS@2+mJRit9IdEp+~^p-x^a`_>yAG;v{~-EOBJ5U)&$t4j^?h-1CC;uhyr5 z|DEs5Z?=!RCzJDkn0K}{iH*N(&LS9nTIAH*8Fj4YULkkC2@Ds`yXmUAXGz>!(MB%Q zCx%$|me}ocCjc@>(W0iBa-ef^^S5zZ=xak3V~sb((PDR8PngoVE^Sm~HAKtyw0#Hw zXXY!mg=ZX6s>#)jVZhCMvXFj$Nu37-a`WHE!z>zMsH(C@6sXVS-IVH_nXiGCOgjKg z@9U0kKpWAvwZ$Cdy1kBEoA@R<;?qzyXDTh?(Qm+XWwDD!6g+~_Z}Twz$RGEC*;OjY zCT*&)@F9PKjlUuHt28M;SodqwLjmZ#2gT^R;uu`AnZ-vMydAKMyjl%j=6c+z_#|;X zG}}d(-;`p=9biqfcy8lsNW}{oFcjL%u9l;tARxVgbk{J2pculG(DeuJ!}7SyU|2%SF4oQU8L_Xn1YkFF=+LXG;ZpK9cFJYF70c*4m9n*v3k~Y zOHIPwC_xRY*|ZVKG9QfW%v#1i>`*#{XVMY5tVcws2Pi8^(4fWE`FJOCS_*wiO*WpN zFbQB-DH6ks$gVC_NB&CyD9;r5iv)8*Z#u-Y(o4=)Ny(f~HvUfw&bH!Y;Vz|HUe|$kX;E37M#@l3A zyJ1O!64G0&f!Kh$Q3rMJar%&Z#no7oiPIL!VyObFnu~&hNPp0*Sfv7~jJU1e-Bg&> zfj8$j^>6-dfR?-7eeP%`U|@ol6`jrP>)|t5vtW7oF!=pp`MkdE`#ff>nI(x5vdN&* zV*D6oYW1ml%gjD%XTdPMP7WnD?%Bk|-I4yGcmU-fys&_z)$8hmsfut~2qt+EjRv0} zxNN^;)7e2r#dgNgL@!9D!|PFU2bGBDhATp!RlyNJKkSJ47|k}uEBKe&vFE>KC_|tD zlC$=p7jC5Gl+c3DMhzM$=(}D?S0j5+DVLS8lKzJqc=&!a7`{isWKevkV%z9(p&AR& zoWmo1X-fVb?xE=dJOBYR8tE0NdS=``t6l<9n92o(QlOb-vKm+oHc}&uJc8gMmbpVB zkDp#feA#f+R*`It>2`4Jp*kO|k&0T7Y-|g6_N>R1O#YWUi%L$zxm)B+i8 zxi1lwNoR&!Cc7zzzQ|rL!?jo}Y?P&qe&I@HvQeK{2}N#TM`5fyKoe z{hwGGxt`0|p8=OjC++S=u|)AH=x;N!pDs=84DLdB4vdvk>LZ;-#V)!U9mVWhVZm46 z(+jWApx&?xux;}B6S01ea;4o*6kF|4umc;(<*<3|Y8nkj07A`YHrTQ#6=m@Kke_igZmvM8Qu0w&~X+~5Ny zt&14I64UJ|T~tuQgtDu96m~~GLQ#5_5a1-PB0toB;1O1L^tet+!t=;f3s8L}+?gg~XKgMLp&LRzg4rx;tS=ZHKbyy8$~dj`uYiS>_T2 zC-q{F{j}^uAu`wa>Z`H5cU%3B4MC`@%H!;qVhBKj1d{;87dqKW-rm(Zz}!1(uqcse z#$^G2HnJ17RJ}u+{$8d3wt|jLW}GivY*3|mD7c%sXjxgh;~%t!Ajekr*Zv0{e`=Q~ zJ4E~_keyuo$?8OK595yK0E+mV!?oO|IH{hDz0WmZHm=B$K_1pe14cXDy1`qZw6gnO0)G)R-ucB_IG^bDkM~?q15_R~Ta;X#YU#LC+cSXL``=*q0Sv3w? zFLyFpVhe${2z4T*y9p{(?l(cMXUPo5RebiOr`2R5_*d_8oo^)uBRQ=p20fu0BBW7c zEUDE8I)d<$Ngjr{R}MG&(+L^;e+a$YeF{qiO$um%p$el-QMajgn%o0<$FeJG1{vul zlyX`wd%(*Y05i08#S`^UX+;g8amTQ@m-oxB*yy)qQ8wKDoszUhFN1<#h z7fT0vtYBCO#BR>e>#PH<)tu~@KhSMWc{LEGaa;TyXPN3!OSt>H+@a@j0R#47M?<4& z+rzT44#C{+X{lIb5hs%m+>rjgsW?t$`b8($r#g2D9ct^rppE#E9z zC4M(UwK7LuFFYHiYB&otIF9*0EW9!t#tVKx7a?_J&FB_8#gjcq~y%p$*F+4erZgM{XI`w*1R0AzMbj#}X^+(t!p zt7W+e!Mk9}+?ZzgCjM)5ci3DxT1`vc7p6f+Qi>#NBD*T&p-H#?wgPp}Wq-7tWG6K` zi@Qm5aBmujs6PD0(YA8q4hX+@GO@Ov>IN=8FZEzxYU3xxR}pv+2`NJ@fy8O9KqnFw z^&e9-PTNBA@1IKkuQ@lZ9`*s-4CR9{80%UfJJnL03yko*%xSf5DM%jF^??J0heudp zp6@^lXX2*yXHIV2LA~H27Q5=X@Mr3>v%+L?t`UBe7rKfw0cmS8v`^X1wu&N-=rSdB zCrdpsoIIm2G`@lu=Cm)n3P+W2<8%fGW9yfjsL7Of z#M2oqCR+yIKi2C1)|{A=Ik{ZoKxIt~Wlzh5qUfI3dp0b^SI;9BAGN8fYZ--k_WiTtc^D(OIKJ zw}06k(V4|Z_omY@ebP`?wB*ZT@AN=gVv_c!=HO2lW`Yk>ow-IqE%Cx%<>XLw{{x*7;slN!f=vzCDrtE^CGD(HoWOnUBbV*?oFQE6!)-P)}cSTQoutS zW9=IWfm3}UZ=jcclTgN^NP|FDi8f|AM4mLdHrg>dg!kU&g|vTE>N0hiykl8PW30Vs zxe<(a=!Zz7q8OFsCdc7wO;**IX1}{TEkCVX@6HOST^!w+p`U&|t3NJ_5l0&%_#KIb zj9@bK7$m#hh%7CbjQ)kC$5P@oFj1gpQu^YGUp2pzbur(kSU`;J_EWoii<7Y99E)y0 z`_@OY6J`1$)KNLFvrkxd7;6WcDo--MXe?uuI&xOkd-$orbEg zy{Qa6#}{0Jw>RJw)Q6|nt#?p)FiBRdf6cO?LIu?ZUFgZIaH73v&xJ3J)qGvx=*0?Q zSffBFg?#|HWF4X{V};=JIfC}{@Mt+jBP@yIT!v-tH16i$l#S zJ{?Q$fHZ}pX-B@}9Ndp^d=wy(622=f`gcs1xUt>E=tOUw)C7EOK)gS-$U}k1w9qE{ z7M3KRj<)LUDyFy;>n6gL^)F?0nYP41k2F8sCliGAjBy~4lHbbsMI9b0{C@vLoI6fD z+RzTmVL%%SI`Tl@CGKxW1tuIeG{?(A7NdaJG$KTXIO?r-qQ3jQH-k?NjWkX(p+ov5 zw(q0T(B7u;`UDr-Stp!3@P~;l6$zqez$Sb3xFk)*#s5Bf*Fsjyi+LF2&8Pca&~a*B9>K$YK( z)IgC4*`N&$EYL{xm^A)Gc}`vA%8q5x$6NJkYM;XuC4>fQ1eh!wHk))w4# zE5S*_8>@#Ai!_rwoCpe_^0An1gwES*)Ii54SAmDke6dx6X~@r{9R34-iiMni-8C}Q zCGmzQh$f<+)2_(xJ0<(d+#b{H;TSwvR6`O^D{unSRArJ<$(f83I{Pq*K+^oh0Q7@ta0) z^Qf5_?a1ySE@F5xNLu0M^xaX~42IO-J;3sW0oXwCx#)A?Bw@6OL6Dve$VGGDDIZ=y zy0!MLjke5DlKi%iw#>jwePKf1X~2v-<4?0Enro=YX+PWB-ptZ8l z+<9NQ&vJvdru@D5lu|!_Hi(;K)-04;$jBSkI!j@Sh}8%pTG`yt9J!rnb$~r`I~l@M z_7&bJ4CT4vpe=TouIAU(mi42vLAZmDuw)Dqa?d^U z+@5DgjkNh=ZowC3Qk5%6_n@S1?r5IH(?~T_H{&Yqk!@-h_`uH5H%DS`8sq7?l7RA} zJgS1nXm{6Owx;{xZ7PK6+6Ci+;A&-!0m@L}^T z^pjim*?*-4XUuci4qE9!Myx~zj%Zh?Y6MoR)Gn}WB|p!&Hme%T6Ln~UU~Edh4IAqe z+w>8%bR!s7Z#$h_l;1c6x)c2kt)y(D#4%$-N4{_*n!F5$YB$n9s0QO!Ta0 zFfMSHEWB-@F^*<;fUeB|Zf|*7W!bM#^;6K`or1MQHKs;v3oW7AO9Pqr7VeU4M9D?f z)zk^%`LMpgQ~DVXDEb6YX@SXnq_2o?uyO6whPy#S@!qQKs!n6+TcGHYAZ zR7N9Bima}F)g6L!xs28w^By|y+_6eZ5xI3E@yiAcECTmgm)`5iU!x`QYP_>+JC^{L z;rGtVp9!ws-D?yqNp-Ly8KDeO%6t%Ngv~yht_r6eWqjG-znWO@00@oo;Xi;93XJ5E z65=o%wYYAZJuqj*6qGNGT7(O0iNOrn43%PG_(?NpAQ%_Y@bsb^(txJ!#ifd!DQrO} znX!=0$or#A9ScPV(kcE{e7HH&Lz|taih%s%m)6OWpxvlSp%Vww?`!WR8~^aNUW`-=VQ`iESDw~-dw(erA=N&tkmwhmqu9-6?jL`&DZ%%Oel zX{XHDB(EdAmUy*YBVtBh_phVZ$BU}`rqPDwGNGx6s$G?3hNvHboQ?*!O`DD8)cyIt z_(hX{W3@n^HYDR)$|Gb<(VJ5J7wPdQ6;s|3glyXQsksuadlDFA^^x79%`g zr1_4PUbJBz0?^>e;9OfmRcP6dwkvGc1JeC7h% zRW_6x+tE>;d>;cNW(5bZiI*)cPu0X9{#lFeC+JDu~S?FeJmEqo|f$iv`e(y386&FJ3eSyDv)XI-kxr(nQ-BXLg}JKI8r8Taj4(u z3qlt8c8)LCPx;h!#cstOT5e6slHr^F+(uK3-qnLEqE97GqSac~hoiIBYNAGC?=~2N)&3^Aw!RDYBZ@CIbY~JLDEyl*O z5D?6z__vYpwc(c(*W`%|sDc&Q-Z{ZdkL#{oYct6aA&zO$(gcZePZ@QSFhMp~Z&v2A z%!NQaCT45RLcq1m$oUt{1PJ}Fna)tnm?;|VsunA+`F@yQxg3Hql>;^e8VOLzd-&H! za)=iJO*h2}(RkY*)?6s_+{Rx)lJc*zcW80fGMuLKi1bHCzZ^ZRQuu~KNQkhBeuhhl zhlfnH|?2d0~#)!^?6%Mde0? zj_GWuxam%>$RD@eg4#pFgrLapptsejCWUx89R>x++FuISCsoY+n$@*rcpTR)#uV-L zRWV0~WLCL&3k`x;<=pLtlq9C-F4HVlovpSxh21@)bC41rZ*VFH<%=<4EeF+`pi@)m zj}kY5J$N}p27fN>;EYo61RNGWBV3tD_f;3vKD6>tuuY(N+69Z`YzF zf>r==wVL0@FBM$1JqzC04%W^t#ywva@PE6M%2WMs=}rr5x9M0IyT~w`iZ`uvjiB!S z?hl7k8CT`!Q|B^R+rnVEiMdcco7w5Nt7su6##sKuHiYVm@hqC)y)Bcc0(<=XAivkQ z9M>9?4jnMN&S>6vZ6v4E?brk6t89H_xiQA|y@XR&JkkcBUJ8+(H&EYrLl1pnlMcls zQtK_Se0 zT|Rc8K?I&MpKL!N%H%}L(AS~ zJ(~sy_}J(1EvJ>)5^+3R$@r;`MSdBUsoEo;1Q?>xi6LSLfMWNki7q#?Ryr5eFH@N* zxHany&_FX##V$o|>07{JsM;MGQ*o?ZPS;m5+d8lzoZdJv1iXH7D5IQCD}I|zV0#Z# zXfCBIBA)8>(r;@H4bg*e_r|PjSnnyW^lE14lK0y~!*xo_YTXPCut$+cRn&Dmp-h4C zbeg`dge)LRry)1zd}Y$YTiTB7(dij}iw>|gxZ94q;-q~h)2<^!$f{E@;G`<{CahyT zW;?6}=hcl_NW2!zO(y-j$I{EH7@03d&s_4qdKGJ*o1Vtb0F|&11ak9fJ@lV&if8_J zX66#YKmXE?RuhsifM?!1h!C-+;o6=F`?0BjX%Zj?G3X~0{KuYsraFV65PB4%?$HS}N~5DKQcmr6jGT$^J? z!G*XC8^3xf{e!4RI;>R>>3+c?;c(Nakk-SjDYLhYaa2z~xCI%rA^FqUaYeIaekPw4 zsGfm5IM!ZxvEa@hg3OP(JL%ULnMs+6BZ)~&`!7?19>hJdO7IML0WAtGu0!%_*^9CLxSb>olSJYT zSFrLx;RYh-jzEL!NA76lFpdwZ67o>j%rcgWv=bNk!J{ssDlR)PN20u9Gt&*wh-bJf zd08i~y@hkjJT<#`?i)3%?WwW6*!q_15|IwX^abTfuT zT!C{F{%!so9Df4}n&P*p*?d*`iSy|oZs|y(wK3Wf6!|-2Wz)Sw{89MbUdsI@&z{vH zHJ!t$ZBl7M{M$>MB3g51xq&-i5n0`Lx-exrjzn=}rYVM~sz=ZZp6S|*!h3X{wDASm z?R*~qEDIYuSouIO4*8UsDkbK|eW}YRDrbYCEZ-f2XQdML`HX&33WOt~HBNfqyAXqH?7yv9ghFB3MNEqOBD9*3Dzu(^wBq>lVrC1HjH zjD31~#F3SO_6vzvt$S=HrN<_wNII(E-=0i6Dn(Xp3J8p1T?t03LK{_BrX-w}m5sq5 zzk2!(&_%#wRyh43Dz}G_ps#ge1N?pbf>wpbvpxIANK!tx*;RuAD?eRe7{kcLWxH;y zr`k9cY{<=mjM(hlafBaboLK;Q;inERGfxj(T^s$aL#n>^?nHjty{F>MNaX*lHX)}P z(3wn87b!$FEpFRmUA$_je?v4aB#9QP3z{YT-)HA6CadG~`eoC03;$A{A{OKZY?74n zI3pm2O|qE#z6$sJE0t`uaEsYKrX>0I*Wv$GN(=t0ly-Ep@f<<}0e!Oo0TKOwDy1g& z=KrTwO7CT4XZwHnQeA)l13vwC|Fckv{m=d%=L?_UPmmk}^q8=kj${D98p08*H09h9A6mYEXf_haJg_onBEJiL4t%)a(8$9!i|74w7 zAz1>U`t?epa3ikU5eEVeu-JjMPk zZ9)kXn3AaX7m9py>J_LIGC9ck;Yp`3csXSzkU3`|!UAQ~dQFm~SL0nXXiLz~5U#l0 zK8~}ulmeO+yAQQ9Jrrx>p6@hr> zGb~bV9T66^D!koGa6-G`@_l(lFn9qDKbkm|SBeecCwe7D57Mlj-~;6;h~|sa^+V_z zLI_Z7LDvR{rC@PXDaxx{zAaWy+XzmHskt^NbE#~XURcUa9Ay%H*`kS2WVc5&!P)9K zunl2)XVCIGH$+C%S4$r!sDT=SGis*`cDLvkg4{-r5J@g>wH?Bnr~QxD_h*5ULB*{$ zllkTFvBdh`D(>bjwIywcs6S7!@Au_s<~->(?@gkZ%g2XRfFBS4SV^{FatUfGw!2bvbzvH zR_2~~f39`mmlciRgpfUKQx)j?J@}&)b_IE(nsab8qW|^zFM@uSfJDy%@OucZd(ql1 zZnC21)~U8Npa3Da{7mntuJ8FGT!a#-jSobifF=q1mYGlC(|ZM^@b`aJb|uhIwSV|B zwv;8GL>ZB-f1wDOBHPert5CKyF~k^QCbA`oGHA7A7cxk8QiPN(QCTLm$QYC@ONNxM z|DCbTFfHFb=WgfBeV*U@-1qmp?|sjl=LSbNXs)>~b>gjpB|rB*pXn~y&hQwsAi4GO zBDj|jg31l?g1xL6$*kA*?l~lINjApbRg{n(77O?OlDkU#F2CI6X_D{PBT)6ntwOC~ z5reir)9W$pNv=Qm`7jHZ31Hbf&0yz z-I4sw!CV6woF+tOGj3Q>pE8p5uwi^eX5TKL60HddjR>jP{GjSyvf#^|1-C9*eZlSG zl$Jr&nQ@QJ80@^6qtuaL{=*?4inUgb&oL`!`yDBZsNA|PC>QvmVtN5;8o~yz-+CFH z8h1+RVs%6w4zo2XHn5z1X8f#}6;HRd&p>W<3C>dYgZjDb@qo~K&&OJ;*@tRzgSY(R z`=_KMLazJytv9)o+98oXD)9E*eX%Q11~_@`qUxBFn%w?=O0fpG_Rh|yY}iJ=q112} z9nrilBWI^wD)+9Gc7$%LQfS7xP2nm1Z-ONo9t5c`8No!g3+2P z`LX7E5P8|*B1S(peVX=I-xmDs6JwYZ<;R&wQg-jzogzl_8gO~niZA`N5Tbs54o zSJv{dz`vdD7G*j%wods4^l9S7K*(byYk1%7?4ghffp*LCwpd-+@F0v4ZZw{+jPN{4 zzwBt+$@o(+mq#}BxYD8I4EeG`bXg7SPL?tib&M6Sn7C0?G08{$AzFr0#?WtgYGVM0 zW!{%c^L3`UlNyQJt4?FfQdr_gm=Ncm7{8Yut!){*>v8l$Sj8duqYhZUSXTD)EWTpp zk#SQ+H?fL$NbM8jCycJewE))A0wVAyglN+`@!EWt)#mO`ct~JZ`NRm`%+H2` zhPk=|!2xA&g$*h;`^{QyvcLy7Bo&)BGJy%udOx5~|Ta~E^L!R)2X+#9ifrh+U?Z+nDdx_Hxh7e6Fv_Ff-K8 z0k=0#efzXNzwQhW7PC{nhckKkx-Uc?D(DNglBoez2(=t}Ku9<+ZJw zz7Z?(I7%$ciHFbcyxfKD?s!jx&3S6?(Tnjgy&E=w|A7xNjZLbCFwd&d}qJ^ z=FxM*3Ts69&)50$ogb^cB=n}SOE3CN?u)k6iB$&`rvs7a%)cePifl;h@3jt|z0DSH zHlaQliz1a(OsN|kVqzA7%-K9W19O3@_?|L(his282MLV9=!5|9lhM>g6x>2tR)A7Dl>0l#*_ z>ydV*nyk5fb!k{6<(xKtic>4~g82<6UHp8k0HEoHb+AL&U@4}bpdJitT4^=T0`ZvJ zcySXT9>sk1pG7>1#-?<5G^VT|a3Nz&L?ClQ7kOT%3v>yXB6K7hZJ>G%Hb`d-az5g- zj-AT~{Hiovl#U>P5dqp0{~CyjL8N^V0(&846`n_(%XDuBsHc%mXlpD;Acr|kRX_Vz3`^Fw!Gd>#OTjuK z=t_APrL83cbPNks#T|*UM!F-Nv6%TcQ?S?nQl2FmlD6UpT~KSg4FDiQM+0UTO?ocS z80lNX9c-|F!7-OvPr4F2bYDL0R4SwT%G~qi@1~w7+{D? zO#y3ngwxqY2jJg{#JDuj6M?oD3>rX9g}*0Sx*NTw-r0hnmvI`n(&u#QuZiTa_Fliw z=_LX#l`5|ou--oaS9*fAM;Wz*D%qb2=A>F_%uU;EfpfG3%WS&;T>DoVZ_j~KdR3~w zF9=Rw@flcf(-!8H9@GnPl+y6T6QJ`GqsmKhq+T}1L;G-|^hkO?>N&)@vcT>|kN6kv z)W7SSb9Z#54H_*a= 3: + file_tag = split_and_strip(toscameta[0]) + 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 + +# Helper: Splits given string by colon and strip +def split_and_strip(string): + result = [x.strip() for x in string.split(':')] + return result[0] + +# Helper: Open CSAR zip file and returns TOSCA.meta file +def open_zip_and_filter(filename): + + try: + with ZipFile(filename, 'r') as zip_object: + file_names = zip_object.namelist() + 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) + 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: + zip_object.close() + +# 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 == 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 == 503: + 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)) + else: + return(create_problem_json(None, "Unknown", code, "Not implemented response code", None)) diff --git a/catalogue-enhanced/src/main.py b/catalogue-enhanced/src/main.py new file mode 100644 index 0000000..31bde13 --- /dev/null +++ b/catalogue-enhanced/src/main.py @@ -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================================================= +# + +import sys + +from flask import Response, Flask +from var_declaration import app, rapp_registry + +# app var need to be initialized +import payload_logging + +# Constants +TEXT_PLAIN='text/plain' + +# Check alive function +@app.route('/', methods=['GET']) +def test(): + return Response("OK", 200, mimetype=TEXT_PLAIN) + +# Delete all rapp definitions +@app.route('/deleteall', methods=['POST']) +def delete_all(): + rapp_registry.clear() + + return Response("All rapp definitions deleted", 200, mimetype=TEXT_PLAIN) + +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 +app.add_api('rapp-catalogue-enhanced.yaml') + +if __name__ == '__main__': + app.run(port=port_number, host="127.0.0.1", threaded=False) diff --git a/catalogue-enhanced/src/maincommon.py b/catalogue-enhanced/src/maincommon.py new file mode 100644 index 0000000..20370f1 --- /dev/null +++ b/catalogue-enhanced/src/maincommon.py @@ -0,0 +1,28 @@ +# ============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 + +# Must exist +apipath = os.environ['APIPATH'] + +# 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) diff --git a/catalogue-enhanced/src/payload_logging.py b/catalogue-enhanced/src/payload_logging.py new file mode 100644 index 0000000..4450ebc --- /dev/null +++ b/catalogue-enhanced/src/payload_logging.py @@ -0,0 +1,60 @@ +# ============LICENSE_START=============================================== +# 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. +# 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/catalogue-enhanced/src/start.sh b/catalogue-enhanced/src/start.sh new file mode 100644 index 0000000..e425208 --- /dev/null +++ b/catalogue-enhanced/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 rapp catalogue enhanced +echo "Path to main.py: "$PWD +python -u main.py diff --git a/catalogue-enhanced/src/util.py b/catalogue-enhanced/src/util.py new file mode 100644 index 0000000..31261fd --- /dev/null +++ b/catalogue-enhanced/src/util.py @@ -0,0 +1,47 @@ +# ============LICENSE_START=============================================== +# Copyright (C) 2022 Nordix Foundation. All rights reserved. +# ======================================================================== +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END================================================= +# + +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 new file mode 100644 index 0000000..e455fe6 --- /dev/null +++ b/catalogue-enhanced/src/var_declaration.py @@ -0,0 +1,24 @@ +# ============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) + +rapp_registry = {} diff --git a/catalogue-enhanced/tests/test_catalogue_enhanced.py b/catalogue-enhanced/tests/test_catalogue_enhanced.py new file mode 100644 index 0000000..8e9ffaf --- /dev/null +++ b/catalogue-enhanced/tests/test_catalogue_enhanced.py @@ -0,0 +1,250 @@ +# ============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 fixture tests the rappcatalogueenhanced module +import json +import time +from unittest_setup import SERVER_URL, setup_env, get_testdata_dir, client + +#Setup env and import paths +setup_env() +from compare_json import compare + +def test_apis(client): + + RESP_TITLE = "The rapp does not exist." + RAPP1 = 'rappcatalogue/rapp1' + RAPP2 = 'rappcatalogue/rapp2' + RAPP1_JSON = 'rapp1.json' + RAPP2_JSON = 'rapp2.json' + + testdata = get_testdata_dir() + + # Header for json payload + header = { + "Content-Type" : "application/json" + } + + # rappcatalogueenhanced hello world + response = client.get(SERVER_URL) + assert response.status_code == 200 + + # Reset rapp catalogue enhanced + response = client.post(SERVER_URL+'deleteall') + assert response.status_code == 200 + assert response.data == b"All rapp definitions deleted" + + # API: Query all rapp ids, shall be empty array + data_response = [ ] + response = client.get(SERVER_URL+'rappcatalogue') + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Query rapp by rapp id , rapp rapp1 not found + data_response = {"title": RESP_TITLE, "status": 404, "instance": "rapp1"} + response=client.get(SERVER_URL+RAPP1) + result=json.loads(response.data) + res=compare(data_response, result) + assert response.status_code == 404 + assert res == True + + # API: Register an rapp: rapp1 + with open(testdata+RAPP1_JSON) as json_file: + json_payload = json.loads(json_file.read()) + response = client.put(SERVER_URL+RAPP1, headers=header, data=json.dumps(json_payload)) + result = json.loads(response.data) + res = compare(json_payload, result) + assert response.status_code == 201 + assert res == True + + # API: Query all rapp ids, shall contain rapp id rapp1 + data_response = ['rapp1'] + response = client.get(SERVER_URL+'rappcatalogue') + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Query rapp by rapp id, rapp rapp1 found + with open(testdata+RAPP1_JSON) as json_file: + data_response = json.load(json_file) + response = client.get(SERVER_URL+RAPP1) + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Filter api list by service type and rapp id, service type provider + with open(testdata+'rapp1_provider_apilist.json') as json_file: + data_response = json.load(json_file) + response = client.get(SERVER_URL+'rappcatalogue/rapp1/provider') + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Filter api list by service type and rapp id, service type invoker + with open(testdata+'rapp1_invoker_apilist.json') as json_file: + data_response = json.load(json_file) + response = client.get(SERVER_URL+'rappcatalogue/rapp1/invoker') + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Delete rapp by rapp id, rapp rapp1 deleted successfully + data_response = b"" + response=client.delete(SERVER_URL+RAPP1) + assert response.status_code == 204 + assert data_response == response.data + + # API: Query all rapp ids, shall be empty array + data_response = [ ] + response = client.get(SERVER_URL+'rappcatalogue') + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Query rapp by rapp id , rapp rapp1 not found + data_response = {"title": RESP_TITLE, "status": 404, "instance": "rapp1"} + response=client.get(SERVER_URL+RAPP1) + result=json.loads(response.data) + res=compare(data_response, result) + assert response.status_code == 404 + assert res == True + + # API: Register an rapp: rapp1 + with open(testdata+RAPP1_JSON) as json_file: + json_payload = json.loads(json_file.read()) + response = client.put(SERVER_URL+RAPP1, headers=header, data=json.dumps(json_payload)) + result = json.loads(response.data) + res = compare(json_payload, result) + assert response.status_code == 201 + assert res == True + + # API: Query all rapp ids, shall contain rapp id rapp1 + data_response = ['rapp1'] + response = client.get(SERVER_URL+'rappcatalogue') + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Query rapp by rapp id, rapp rapp1 found + with open(testdata+RAPP1_JSON) as json_file: + data_response = json.load(json_file) + response = client.get(SERVER_URL+RAPP1) + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Update an rapp: rapp1 + with open(testdata+RAPP1_JSON) as json_file: + json_payload = json.loads(json_file.read()) + response = client.put(SERVER_URL+RAPP1, headers=header, data=json.dumps(json_payload)) + result = json.loads(response.data) + res = compare(json_payload, result) + assert response.status_code == 200 + assert res == True + + # API: Query rapp by rapp id, rapp rapp1 found + with open(testdata+RAPP1_JSON) as json_file: + data_response = json.load(json_file) + response = client.get(SERVER_URL+RAPP1) + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Register an rapp: rapp2 + with open(testdata+RAPP2_JSON) as json_file: + json_payload = json.loads(json_file.read()) + response = client.put(SERVER_URL+RAPP2, headers=header, data=json.dumps(json_payload)) + result = json.loads(response.data) + res = compare(json_payload, result) + assert response.status_code == 201 + assert res == True + + # API: Query rapp by rapp id, rapp rapp2 found + with open(testdata+RAPP2_JSON) as json_file: + data_response = json.load(json_file) + response = client.get(SERVER_URL+RAPP2) + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Filter api list by service type and rapp id, service type provider + with open(testdata+'rapp2_provider_apilist.json') as json_file: + data_response = json.load(json_file) + response = client.get(SERVER_URL+'rappcatalogue/rapp2/provider') + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Filter api list by service type and rapp id, service type invoker + with open(testdata+'rapp2_invoker_apilist.json') as json_file: + data_response = json.load(json_file) + response = client.get(SERVER_URL+'rappcatalogue/rapp2/invoker') + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Query all rapp ids, shall contain rapp id rapp1 and rapp2 + data_response = ['rapp1', 'rapp2'] + response = client.get(SERVER_URL+'rappcatalogue') + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Delete rapp by rapp id, rapp rapp1 deleted successfully + data_response = b"" + response=client.delete(SERVER_URL+RAPP1) + assert response.status_code == 204 + assert data_response == response.data + + # API: Query rapp by rapp id , rapp rapp1 not found + data_response = {"title": RESP_TITLE, "status": 404, "instance": "rapp1"} + response=client.get(SERVER_URL+RAPP1) + result=json.loads(response.data) + res=compare(data_response, result) + assert response.status_code == 404 + assert res == True + + # API: Query all rapp ids, shall contain rapp id rapp2 + data_response = ['rapp2'] + response = client.get(SERVER_URL+'rappcatalogue') + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True + + # API: Query rapp by rapp id, rapp rapp2 found + with open(testdata+RAPP2_JSON) as json_file: + data_response = json.load(json_file) + response = client.get(SERVER_URL+RAPP2) + result = json.loads(response.data) + res = compare(data_response, result) + assert response.status_code == 200 + assert res == True diff --git a/catalogue-enhanced/tests/unittest_setup.py b/catalogue-enhanced/tests/unittest_setup.py new file mode 100644 index 0000000..e14d3ae --- /dev/null +++ b/catalogue-enhanced/tests/unittest_setup.py @@ -0,0 +1,57 @@ +# ============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================================================= +# + +# Setting up dir and env for unit test of rappcatalogueenhanced +import sys +import os +import pytest + +#Server port and base path +PORT_NUMBER="9096" +HOST_IP="localhost" +SERVER_URL="http://"+HOST_IP+":"+PORT_NUMBER+"/" + +#Dir for json test data files +testdata="" + +def setup_env(): + global testdata + cwd=os.getcwd()+"/" + + if 'TESTS_BASE_PATH' in os.environ: + cwd = os.environ['TESTS_BASE_PATH']+"/" + + testdata = cwd+"../../catalogue-enhanced-test/jsonfiles/" + + #Env var to setup version and host logging + os.environ['APIPATH'] = cwd+"../api/" + os.environ['REMOTE_HOSTS_LOGGING'] = "ON" + + sys.path.append(os.path.abspath(cwd+'../src')) # include: src + sys.path.append(os.path.abspath(cwd+'../../catalogue-enhanced-test/common')) # include: commons + + os.chdir(cwd+"../src") + +def get_testdata_dir(): + return testdata + +#Test client for rest calls +@pytest.fixture +def client(): + from main import app + with app.app.test_client() as c: + yield c diff --git a/catalogue-enhanced/tox.ini b/catalogue-enhanced/tox.ini new file mode 100644 index 0000000..e21b9b5 --- /dev/null +++ b/catalogue-enhanced/tox.ini @@ -0,0 +1,34 @@ +# ================================================================================== +# 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 = code +minversion = 2.0 +skipsdist = true + +# basic test and coverage job +[testenv:code] +basepython = python3.8 +deps= + pytest + coverage + pytest-cov + connexion + +setenv = TESTS_BASE_PATH = {toxinidir}/tests +commands = + pytest --cov-append --cov {toxinidir} --cov-report xml --cov-report term-missing --cov-report html -- 2.16.6