add tempest report and logs parser initial script
Change-Id: I82bb5fdcc78e293359698b21537bf96be6e8451c
Related-prod: PRODX-19554
diff --git a/tempest_tests_resources/README.md b/tempest_tests_resources/README.md
new file mode 100644
index 0000000..551104d
--- /dev/null
+++ b/tempest_tests_resources/README.md
@@ -0,0 +1,19 @@
+Tempest Resources Parser
+======================
+
+This tool creating machine readable YAML file with all resources used in Tempest tests
+
+How to use
+----------
+
+Update your env variabled or add report and result file to artifacts dir
+```
+export REPORT_NAME='' \
+export TEMPORARY_FILE_NAME='' \
+export RESULT_FILE_NAME='' \
+export TEMPEST_REPORT_XML=''
+```
+
+Run report parser script:
+
+``python3 report_parser.py``
diff --git a/tempest_tests_resources/__init__.py b/tempest_tests_resources/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest_tests_resources/__init__.py
diff --git a/tempest_tests_resources/config.py b/tempest_tests_resources/config.py
new file mode 100644
index 0000000..9a3a450
--- /dev/null
+++ b/tempest_tests_resources/config.py
@@ -0,0 +1,10 @@
+import os
+
+REPORT_NAME = os.environ.get('REPORT_NAME', 'artifacts/tempest.log')
+TEMPEST_REPORT_XML = os.environ.get('TEMPEST_REPORT_XML', 'artifacts/tempest_report.xml')
+
+# Results machine readable file
+RESOURCES_FILE_NAME = os.environ.get('RESULT_FILE_NAME', 'artifacts/tempest_resources.yaml')
+
+# Temporary file
+TEMPORARY_FILE_NAME = os.environ.get('TEMPORARY_FILE_NAME', 'artifacts/tempest_temporary')
diff --git a/tempest_tests_resources/report_parser.py b/tempest_tests_resources/report_parser.py
new file mode 100644
index 0000000..4c43301
--- /dev/null
+++ b/tempest_tests_resources/report_parser.py
@@ -0,0 +1,314 @@
+import re
+import subprocess
+import json
+import yaml
+import os
+
+import xml.etree.ElementTree as ET
+
+import config
+
+
+# config.py
+REPORT_NAME = config.REPORT_NAME
+TEMPORARY_FILENAME = config.TEMPORARY_FILE_NAME
+TEMPEST_TESTS_RESOURCES = config.RESOURCES_FILE_NAME
+
+
+def simplify_logfile(report_name, temporary_filename):
+ """ Simplify full tempest log file and write it to temp file
+ After simplifying temp file looks like:
+ ...
+ Request...
+ Req_headers...
+ Body: {<request_body>}
+ Response:
+ Body: {<response_body>}
+ ---
+ ...
+ :param report_name: full tempest logs file
+ :param temporary_filename: simplified file
+ """
+ run_cmd = f"grep -vE '(auth/token|keystoneauth|connectionpool)' " \
+ f"{report_name} | grep -A4 -E '( POST| DELETE| PUT)' " \
+ f"> {temporary_filename}"
+ subprocess.check_output(run_cmd, shell=True)
+
+
+def get_request_response(temporary_filename):
+ """ Get request+testname+response
+ :param temporary_filename: simplified report filename
+ :return: list with lines that contains request and response
+ """
+ with open(temporary_filename, 'r') as temp_file:
+ request = []
+ lines = temp_file.readlines()
+ for line in lines:
+ if line.startswith('--'):
+ yield request
+ request = []
+ else:
+ request.append(line)
+
+
+def _get_test_name(request,
+ methods_skip_list=[
+ 'tearDownClass',
+ 'tearDown',
+ '_run_cleanups',
+ 'setUp',
+ 'setUpClass',
+ 'tearDownClass',]):
+ """
+ :param request: request body
+ :param methods_skip_list: what methods to skip
+ :return:
+ """
+ # Skip list to process requests from tests only
+
+ try:
+ # regex for: (ClassName:test_name)
+ test_name = re.search(r'\((\w+:.+\))', request)[0][1:-1]
+
+ # Skip if method name in skip list
+ if test_name.split(':')[1] in methods_skip_list:
+ return
+ return test_name.replace(':', '.')
+ except TypeError:
+ pass
+ # TODO(imenkov): add logging
+ # print(f"request: {request} failed to find name")
+
+
+def _get_response_body(response):
+ """ Method to get response body as dict
+ :param response: line with response
+ :return: dict with body or empty dict if
+ body is not readable
+ """
+ try:
+ # regex to search dict in response
+ body = re.search(
+ r'(\{.[a-zA-Z]+).+(}|])',
+ response)[0]
+ if body:
+ if 'badRequest' not in body:
+ res = json.loads(body)
+ return res
+ except Exception:
+ return response.split(
+ '_log_request_full')[0].strip(' ').replace(
+ "Body: b", "").strip("\'")
+
+
+def _get_openstack_service_name(request):
+ #TODO IMENKOV FIX ME
+ service_name = re.search(
+ r'(?<=\:\/\/).+?(?=(\.))',
+ request)[0]
+ return service_name
+
+
+def _get_status_code_and_method(request):
+ status_code = re.search(
+ r'(?<=\): ).+?(?=( http))',
+ request)[0]
+ return status_code.split()
+
+
+def _get_request_id(request):
+ """
+ :param request: request line from logs
+ :return: request-id like: req-93636f78-031b-41bc-abb5-9533ab7a3df4
+ """
+ try:
+ req_id = re.search(
+ r"(?<=\[)req-.+?(?= \])",
+ request)
+ if req_id:
+ return req_id[0]
+ except TypeError:
+ # TODO(imenkov) add logging to track not covered requests
+ # print(f"Request ID not found for request: {request}")
+ return 'req_id_not_found'
+
+
+def _get_resource_name_from_request_body(request, os_resource_name=None):
+ """
+ :param request: request body
+ :param os_resource_name: OpenStack resource name (server/volume e.t.c)
+ :return: resource name (tempest-*) or `resource_name_not_defined`
+ """
+ body = _get_response_body(request)
+ try:
+ name = body.get(os_resource_name, {}).get('name',
+ 'resource_name_not_defined')
+ return name
+ except AttributeError:
+ return 'resource_name_not_defined'
+
+
+def generate_tests_resources():
+ """
+ <test identifier>:
+ status: PASSED|FAILED
+ resources:
+ <openstack-service name (nova|neutron|glance|keystone)>:
+ <resource-name (port|server|security-group|router)>:
+ <request-id (req-xxxx) >:
+ name: <resource name (test-port-mytest|test-vm-)>
+ id/uuid: <resource id>
+ # requst: <exact request>
+ http:
+ error: <if exists>
+ status_code: <>
+ """
+ result = {}
+
+ for request in get_request_response(TEMPORARY_FILENAME):
+
+ # Get test name from request
+ test_name = _get_test_name(request[0])
+ if not test_name:
+ continue
+
+ if not result.get(test_name):
+ result[test_name] = {"status": None,
+ "resources": {}}
+
+ status_and_method = _get_status_code_and_method(request[0])
+ status_code = status_and_method[0]
+ http_method = status_and_method[1]
+
+ openstack_service = _get_openstack_service_name(request[0])
+ if not result[test_name]['resources'].get(openstack_service):
+ result[test_name]['resources'][openstack_service] = {}
+
+
+ response_body = _get_response_body(request[-1])
+ if not isinstance(response_body, dict):
+ request_id = _get_request_id(request[0])
+
+ # Check request body
+ os_resource_name = _get_resource_name_from_request_body(
+ request[2])
+
+ if not result[test_name]['resources'][
+ openstack_service].get(os_resource_name):
+ result[test_name]['resources'][openstack_service][os_resource_name] = {}
+
+ result[test_name]['resources'][
+ openstack_service][os_resource_name][request_id] = {
+ 'http': {'response_body': response_body,
+ 'status_code': status_code,
+ 'http_method': http_method}}
+ continue
+
+ for os_resource_name in response_body.keys():
+
+ if not result[test_name]['resources'][openstack_service].get(
+ os_resource_name):
+ result[test_name]['resources'][openstack_service][
+ os_resource_name] = {}
+
+ request_id = _get_request_id(request[0])
+
+ if not result[test_name]['resources'][openstack_service][
+ os_resource_name].get(request_id):
+
+ result[test_name]['resources'][openstack_service][
+ os_resource_name][request_id] = {
+ 'http': {'status_code': status_code,
+ 'http_method': http_method}}
+
+ #TODO (IMENKOV) ADD 400/500
+ # Check that response is dict
+ # In some cases response can contain strings as
+ # instance logs, hash or lists
+ if isinstance(response_body[os_resource_name], dict):
+ resource_id = response_body[os_resource_name].get('id')
+ resource_name = response_body[os_resource_name].get('name')
+ else:
+ resource_id = None
+ resource_name = None
+
+ # Add resource id to yaml
+ # ...
+ # testname:
+ # os_resource_name:
+ # - resource_id1
+ # - resource_id2
+ # ...
+
+ if resource_id:
+ result[test_name]['resources'][openstack_service][
+ os_resource_name][request_id]['id'] = resource_id
+ if not resource_name:
+ resource_name = _get_resource_name_from_request_body(
+ request[2], os_resource_name)
+ result[test_name]['resources'][openstack_service][
+ os_resource_name][request_id]['name'] = resource_name
+ else:
+ result[test_name]['resources'][openstack_service][
+ os_resource_name][request_id]['name'] = resource_name
+
+ # Check if resource doesn't contain IDs - cleanup it
+ if not result[test_name]['resources'][openstack_service][
+ os_resource_name][request_id]:
+ del result[test_name]['resources'][openstack_service][
+ os_resource_name][request_id]
+
+ return result
+
+
+def add_test_result_from_xml(report_name, result):
+ tree = ET.parse(report_name)
+ root = tree.getroot()
+
+ if root[0].tag == 'testsuite':
+ root = root[0]
+
+ for child in root:
+ classname = child.attrib['classname']
+ name = child.attrib['name']
+ if classname and name:
+ short_classname = classname.split('.')[-1]
+ short_name = name.split('[')[0]
+ short_test_name = f"{short_classname}.{short_name}"
+
+ # (imenkov) mb use it as key
+ full_test_name = f"{classname}.{name}"
+
+ try:
+ test_status = child[0].tag
+ except IndexError:
+ test_status = 'passed'
+
+ if short_test_name and short_test_name in result:
+ # TODO(imenkov): how to avoid issue
+ # we can see in report 2 tests:
+ # test_cannot_create_MX_with_1_empty_preference
+ # test_cannot_create_MX_with_2_minus_zero_preference
+ #
+ # but they logged as one:
+ # RecordsetValidationTest:test_cannot_create_MX_with
+ if not result[short_test_name].get('full_test_name'):
+ result[short_test_name]['full_test_name'] = []
+ result[short_test_name]['full_test_name'].append(full_test_name)
+ result[short_test_name]['status'] = test_status
+
+
+def delete_temporary_file(path_to_temp_file):
+ os.remove(path_to_temp_file)
+
+
+simplify_logfile(REPORT_NAME, TEMPORARY_FILENAME)
+result = generate_tests_resources()
+add_test_result_from_xml(config.TEMPEST_REPORT_XML, result)
+
+# NOTE(imenkov): currently skipped for debug
+# delete_temporary_file(TEMPORARY_FILENAME)
+
+# Write results to yaml file
+with open(TEMPEST_TESTS_RESOURCES, 'w') as res_file:
+ yaml.dump(result, res_file)