From bc2278dfed01a15e0647a94005e1c53b01e7576f Mon Sep 17 00:00:00 2001 From: Ashutosh Mishra Date: Wed, 26 Feb 2025 15:23:15 +0000 Subject: [PATCH] Add Subscription conformance test in Xtesting This patch adds a new conformance test for Subscription based on ETSI NFV-TST 010 specification with Xtesting. Change-Id: Ibd3381db6fdcd4ef0909dcf46d364db6096d219f Signed-off-by: Ashutosh Mishra Issue-ID: SMO-184 --- docs/developer-guide.rst | 74 ++++++- .../SOL005/CNFPrecondition/packageTest.sh | 15 +- tacker/tacker/tests/xtesting/callback.service | 10 + .../tacker/tests/xtesting/callback_server2_pep8.py | 230 +++++++++++++++++++++ tacker/tacker/tests/xtesting/gunicorn.conf.py | 15 ++ tacker/tacker/tests/xtesting/testcases.yaml | 12 ++ 6 files changed, 347 insertions(+), 9 deletions(-) create mode 100644 tacker/tacker/tests/xtesting/callback.service create mode 100644 tacker/tacker/tests/xtesting/callback_server2_pep8.py create mode 100644 tacker/tacker/tests/xtesting/gunicorn.conf.py diff --git a/docs/developer-guide.rst b/docs/developer-guide.rst index 567409d..9986736 100644 --- a/docs/developer-guide.rst +++ b/docs/developer-guide.rst @@ -92,7 +92,7 @@ The following steps are the procedure of API conformance test according to the s .. code:: bash - $ cp /tmp/o2/tacker/tacker/tests/xtesting/testcases.yaml ./xtesting-py3/lib/python3.8/site-packages/xtesting/ci/ + $ cp /tmp/o2/tacker/tacker/tests/xtesting/testcases.yaml ./xtesting-py3/lib/python3.10/site-packages/xtesting/ci/ .. note:: @@ -118,6 +118,7 @@ The following steps are the procedure of API conformance test according to the s $ cp ./api-tests/SOL003/VNFLifecycleManagement-API/jsons/healVnfRequest.json ./jsons/healVnfcRequest.json $ mkdir schemas $ cp ./api-tests/SOL003/VNFLifecycleManagement-API/schemas/vnfInstance.schema.json ./schemas + $ cp ./api-tests/SOL003/VNFLifecycleManagement-API/jsons/lccnSubscriptionRequest.json ./jsons/ * Preconditioning for test execution @@ -141,6 +142,8 @@ The following steps are the procedure of API conformance test according to the s $ sudo apt-get install dos2unix $ sudo apt install jq + $ sudo apt-get install gunicorn + $ pip install flask 4. Execute script 'packageTest.sh' for package creation and uploading. @@ -238,12 +241,50 @@ The following steps are the procedure of API conformance test according to the s ] } - 7. Start kubectl proxy. + 7. Change 'vnfdId' in the file 'lccnSubscriptionRequest.json' as below.' + + .. code:: bash + + { + "filter": { + "vnfInstanceSubscriptionFilter": { + "vnfdIds": [ + "2e27397f-87e1-46ff-aff8-02ffeb40f628" # Update value here + ] + } + }, + "callbackUri": "${callback_uri}:${callback_port}/${callback_endpoint}" + } + + 8. Start kubectl proxy. .. code:: bash $ kubectl proxy --port=8080 & + 9. Create Notification Server Using Callback Uri. + + .. code:: bash + + $ mkdir ~/notification_server + $ cd ~/notification_server + $ cp /tmp/o2/tacker/tacker/tests/xtesting/callback_server2_pep8.py . + $ cp /tmp/o2/tacker/tacker/tests/xtesting/gunicorn.conf.py . + + .. note:: + + Replace the Path and IP's in the callback_server2_pep8.py and gunicorn.conf.py files. + + 10. Create callback service. + + .. code:: bash + + $ sudo cp /tmp/o2/tacker/tacker/tests/xtesting/callback.service /etc/systemd/system/ + $ systemctl daemon-reload && systemctl enable callback.service + $ systemctl start callback.service + $ systemctl status callback.service + + * Testing steps 1. Verify Vnflcm Create, Instantiate and Heal. @@ -254,7 +295,15 @@ The following steps are the procedure of API conformance test according to the s $ . xtesting-py3/bin/activate $ sudo xtesting-py3/bin/run_tests -t cnf-lcm-validation - 2. Verify getting all pods and getting specific pod. + 2. Verify Subscription. + + .. code:: bash + + $ cd ~/tacker/tacker/tests/xtesting/ + $ . xtesting-py3/bin/activate + $ sudo xtesting-py3/bin/run_tests -t cnf-subscription-validation + + 3. Verify getting all pods and getting specific pod. .. code:: bash @@ -331,4 +380,23 @@ The following steps are the procedure of API conformance test according to the s If any update in the package with respect to name and namespace, then the name and namespace variables in the file '~/tacker/tacker/tests/xtesting/api-tests/SOL003/CNFDeployment/environment/variables.txt' need to be updated accordingly. +* Troubleshoot + + error: Not authorized. + + Replace X-Subject-Token value with ${AUTHORIZATION_TOKEN} variable in following keywords in api-tests/SOL003/VNFLifecycleManagement-API/VnfLcmMntOperationKeywords.robot file - + 1. POST Create a new vnfInstance + 2. POST instantiate individual vnfInstance + 3. POST Heal VNF + + Create token using below command- + + .. code:: bash + + $ curl -X POST -H 'Content-Type:application/json' --data '{"auth": {"scope": + {"project": {"domain": {"id": "default"}, "name": "nfv"}}, "identity": + {"password": {"user": {"domain": {"id": "default"}, "password": + "devstack", "name": "nfv_user"}}, "methods": ["password"]}}}' \ + -i http://localhost/identity/v3/auth/tokens + .. _ETSI NFV-TST 010: https://www.etsi.org/deliver/etsi_gs/NFV-TST/001_099/010/02.06.01_60/gs_NFV-TST010v020601p.pdf diff --git a/tacker/tacker/tests/xtesting/api-tests/SOL005/CNFPrecondition/packageTest.sh b/tacker/tacker/tests/xtesting/api-tests/SOL005/CNFPrecondition/packageTest.sh index 20a21aa..7207a09 100755 --- a/tacker/tacker/tests/xtesting/api-tests/SOL005/CNFPrecondition/packageTest.sh +++ b/tacker/tacker/tests/xtesting/api-tests/SOL005/CNFPrecondition/packageTest.sh @@ -98,6 +98,10 @@ eval "$Command" #change variable names and values to adapt our test sed -i 's/vnfdId=${Descriptor_ID}/vnfdId=${vnfdId}/g' ../../SOL003/VNFLifecycleManagement-API/VnfLcmMntOperationKeywords.robot +sed -i 's/ Create Mock Expectation ${notification_request} ${notification_response}/\# Create Mock Expectation ${notification_request} ${notification_response}/g' ../../SOL003/VNFLifecycleManagement-API/VnfLcmMntOperationKeywords.robot + +sed -i 's/ Clear Requests ${callback_endpoint}/\# Clear Requests ${callback_endpoint}/g' ../../SOL003/VNFLifecycleManagement-API/VnfLcmMntOperationKeywords.robot + #comment out test cases in api-tests which are unnecessary for conformance test robotFile=../../SOL003/VNFLifecycleManagement-API/VNFInstances.robot lineNo=`cat -n $robotFile | sed -n '/POST Create a new vnfInstance/,$p' | grep -E '^([0-9]|[[:space:]])+$' | head -1` @@ -126,12 +130,11 @@ insertSteps="*** comment ***" Command="sed -i '$((lineNo))a $insertSteps' $robotFile" eval "$Command" -#modify api-tests code so that vnfInstanceId is treated as global variable -# TODO: After the modification is officially done in api-tests by ETSI NFV TST, we need to remove below step. -robotFile=../../SOL003/VNFLifecycleManagement-API/VnfLcmMntOperationKeywords.robot -lineNo=`cat -n $robotFile | sed -n '/POST Create a new vnfInstance/,$p' | grep -E '^([0-9]|[[:space:]])+$' | head -1` -insertSteps="\ \${res_body}= Get From Dictionary \${outputResponse} body\n \${res_id}= Get From Dictionary \${res_body} id\n Set Global Variable \${vnfInstanceId} \${res_id}" -Command="sed -i '$((lineNo))i $insertSteps' $robotFile" +#comment out test cases in api-tests which are unnecessary for conformance test +robotFile=../../SOL003/VNFLifecycleManagement-API/Subscriptions.robot +lineNo=`cat -n $robotFile | sed -n '/POST Create a new subscription/,$p' | grep -E '^([0-9]|[[:space:]])+$' | head -1` +insertSteps="*** comment ***" +Command="sed -i '$((lineNo))a $insertSteps' $robotFile" eval "$Command" exit 0 diff --git a/tacker/tacker/tests/xtesting/callback.service b/tacker/tacker/tests/xtesting/callback.service new file mode 100644 index 0000000..c3d32e8 --- /dev/null +++ b/tacker/tacker/tests/xtesting/callback.service @@ -0,0 +1,10 @@ +[Unit] +Description=Callback URI Service + +[Service] +WorkingDirectory=/opt/stack/notification_server/ +ExecStart=/usr/bin/gunicorn --config ./gunicorn.conf.py callback_server2_pep8:app +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/tacker/tacker/tests/xtesting/callback_server2_pep8.py b/tacker/tacker/tests/xtesting/callback_server2_pep8.py new file mode 100644 index 0000000..8f98eab --- /dev/null +++ b/tacker/tacker/tests/xtesting/callback_server2_pep8.py @@ -0,0 +1,230 @@ +import logging +import logging.handlers +import requests +import base64 +from flask import Flask, request, jsonify + +app = Flask(__name__) +base_uri = "/your-callback-endpoint" # Replace with your desired path + +# Authentication credentials (replace with your actual values) +notification_username = "nfv_user" +notification_password = "devstack" +token_endpoint = "http://10.0.0.51/identity/v3/auth/tokens" # Replace with the IP address of the server where the Keystone service is running + +# Logging configuration +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# File logging configuration +fh = logging.handlers.RotatingFileHandler( + '/opt/stack/logs/gunicorn/access.log', maxBytes=10485760, backupCount=5 +) + +# Formatter with date and time +formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") +fh.setFormatter(formatter) # Apply formatter to the file handler +logger.addHandler(fh) + + +# Helper function to create authentication headers +def get_oauth2_token(client_id, client_password, token_endpoint): + """Retrieves an OAuth2 token using client credentials grant.""" + data = { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "name": client_id, + "password": client_password, + "domain": {"id": "default"}, + } + }, + }, + "grant_type": "client_credentials", + "client_id": client_id, + "client_secret": client_password, + } + } + logger.info("Authentication token data: %s", data) + + headers = { + "Authorization": ( + "Basic " + + base64.b64encode( + (client_id + ":" + client_password).encode() + ) + .decode() + ), + "Content-Type": "application/json", + } + logger.info("Authentication token headers: %s", headers) + + try: + logger.info("Token endpoint: %s", token_endpoint) + response = requests.post(token_endpoint, headers=headers, json=data) + x_subject_token = response.headers.get("X-Subject-Token") + if response.status_code == 201: + logger.info("Token creation successful: %s", x_subject_token) + return x_subject_token + else: + logger.error( + "Token creation failed with status code: %s, response: %s", + response.status_code, + response.text, + ) + except requests.exceptions.RequestException as e: + logger.error("Error retrieving OAuth2 token: %s", str(e)) + return None + + +def create_auth_headers(access_token): + return {"X-Auth-Token": f"{access_token}"} + + +def create_version_headers(): + # Default version if not provided + # version = request.headers.get("Version", "2.0.0") + version = "2.0.0" + logger.info("Version: %s", version) + return {"Version": f"{version}"} + + +@app.route(base_uri, methods=["GET"]) +def callback_get(): + # Get OAuth2 token using provided authentication data + logger.info("Received GET request with valid authentication") + + # Function to extract and validate authorization headers from the request + logger.info("Authentication headers: %s", request.headers) + # logger.info("Authentication json: %s", request.json) + + auth_type = request.headers.get("Authorization", "").split(" ")[0].upper() + logger.info("Received authentication type: %s", auth_type) + + if auth_type == "BASIC": + try: + encoded_creds = ( + request.headers.get("Authorization", "").split(" ")[1] + ) + logger.info("Authentication encoded_creds: %s", encoded_creds) + decoded_creds = base64.b64decode(encoded_creds).decode() + logger.info("Authentication decoded_creds: %s", decoded_creds) + username, password = decoded_creds.split(":") + logger.info("Authentication Username: %s", username) + logger.info("Authentication Password: %s", password) + + if username == notification_username\ + and password == notification_password: + logger.info("GET request authentication successful") + return 'GET request authentication successful', 204 + else: + return 'GET request authentication fail', 401 + except Exception as e: + logger.error("Invalid Basic authentication format: %s", e) + raise Exception("Invalid authentication") + else: + return 'GET request authentication successful', 204 + #logger.error("Unsupported authentication type: %s", auth_type) + #raise Exception("Unsupported authentication method") + + +@app.route(base_uri, methods=['POST']) +def callback_post(): + logger.info("Received POST request with valid authentication and data:\ + %s", request.json) + # logger.info("Authentication POST headers: %s", request.headers) + + try: + data = request.json # Assuming JSON notification payload + # Extract VNF instance ID + vnfInstanceId = ( + data['alarm']['_links']['objectInstance']['href'].split('/')[-1] + ) + logger.info("VNF Instance ID: %s", vnfInstanceId) + + if vnfInstanceId: + heal_data = { + "cause": "healing" # Default cause + } + heal_data["additionalParams"] = { + "all": False # Default "all" value + } + + # Extract VNFC instance IDs + vnfcInstanceIds = data['alarm']['vnfcInstanceIds'] + logger.info("VNFC Instance ID: %s", vnfcInstanceIds) + if vnfcInstanceIds: + heal_data["vnfcInstanceId"] = vnfcInstanceIds + + logger.info("Heal request with data: %s", heal_data) + + # Get OAuth2 token using provided authentication data + auth_type = ( + request.headers.get('Authorization', '') + .split(" ")[0] + .upper() + ) + logger.info("Received authentication type: %s", auth_type) + if auth_type == "BASIC": + encoded_creds = ( + request.headers.get('Authorization', '') + .split(" ")[1] + ) + logger.info("Authentication encoded_creds: %s", encoded_creds) + decoded_creds = base64.b64decode(encoded_creds).decode() + logger.info("Authentication decoded_creds: %s", decoded_creds) + client_id, client_password = decoded_creds.split(":") + logger.info("Authentication client_id: %s", client_id) + logger.info( + "Authentication client_password: %s", client_password + ) + + access_token = get_oauth2_token( + client_id, client_password, token_endpoint + ) + if access_token: + # Merge authentication and version headers + auth_headers = create_auth_headers(access_token) + version_headers = create_version_headers() + combined_headers = {**auth_headers, **version_headers} + logger.info("Heal request headers: %s", combined_headers) + else: + logger.error( + "Failed to retrieve OAuth2 token for heal request" + ) + return jsonify({'error': 'Failed to retrieve\ + access token'}), 401 + else: + logger.error("Unsupported authentication type or missing data") + return jsonify({'error': 'Unsupported authentication or\ + missing data'}), 401 + + # Send heal request with authentication headers + base_url = "http://10.0.0.51:9890/vnflcm/v2/vnf_instances/" # Replace with the IP address of the server where the tacker service is running + path = f"{vnfInstanceId}/heal" + url = base_url + path + + # Authentication headers with heal_data + response = requests.post( + url, json=heal_data, headers=combined_headers + ) + # Authentication headers without heal_data + # response = requests.post(url, headers=combined_headers) + + if response.status_code == 202: + logger.info( + "Heal request accepted by Tacker for VNF instance %s", + vnfInstanceId + ) + return 'Heal request received', 204 + else: + logger.error( + "Error sending heal request for VNF instance %s: %s", + vnfInstanceId, + response.text + ) + except Exception as e: + logger.error("Error processing POST request: %s", str(e)) + return jsonify({"error": "Internal server error"}), 500 diff --git a/tacker/tacker/tests/xtesting/gunicorn.conf.py b/tacker/tacker/tests/xtesting/gunicorn.conf.py new file mode 100644 index 0000000..703525f --- /dev/null +++ b/tacker/tacker/tests/xtesting/gunicorn.conf.py @@ -0,0 +1,15 @@ +bind = "10.0.0.194:5000" # Replace with your desired host and port +accesslog = '/opt/stack/logs/gunicorn/access.log' # Log requests to stdout +errorlog = '/opt/stack/logs/gunicorn/error.log' # Log errors to stderr +# access-logfile = '/opt/stack/logs/gunicorn/access.log' +# error-logfile = '/opt/stack/logs/gunicorn/error.log' +workers = 2 + +# accesslog = '-' # Log requests to stdout +# errorlog = '-' # Log errors to stderr + +# Restart on worker failure (5 attempts in 5 seconds) +max_requests = 1000 # Restart after a certain number of requests +max_requests_jitter = 500 # Add randomness to restart timing +timeout = 60 # Restart if a worker doesn't respond for 60 seconds +graceful_timeout = 30 # Time to finish existing requests before restart diff --git a/tacker/tacker/tests/xtesting/testcases.yaml b/tacker/tacker/tests/xtesting/testcases.yaml index ab33f53..bb61ec3 100644 --- a/tacker/tacker/tests/xtesting/testcases.yaml +++ b/tacker/tacker/tests/xtesting/testcases.yaml @@ -32,3 +32,15 @@ tiers: suites: - >- /opt/stack/tacker/tacker/tests/xtesting/api-tests/SOL003/CNFDeployment/IndividualCnfLcmOperationOccurrence.robot + - case_name: cnf-subscription-validation + project_name: smo + criteria: 100 + blocking: true + clean_flag: false + description: '' + run: + name: robotframework + args: + suites: + - >- + /opt/stack/tacker/tacker/tests/xtesting/api-tests/SOL003/VNFLifecycleManagement-API/Subscriptions.robot -- 2.16.6