Merge "bugfix: _convert_duration error when encounter ':00:00.078149'"
[releng.git] / utils / test / scripts / mongo_to_elasticsearch.py
1 #! /usr/bin/env python
2 import logging
3 import argparse
4 import shared_utils
5 import json
6 import urlparse
7 import uuid
8 import os
9 import subprocess
10 import datetime
11
12 logger = logging.getLogger('mongo_to_elasticsearch')
13 logger.setLevel(logging.DEBUG)
14 file_handler = logging.FileHandler('/var/log/{}.log'.format(__name__))
15 file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
16 logger.addHandler(file_handler)
17
18
19 def _get_dicts_from_list(testcase, dict_list, keys):
20     dicts = []
21     for dictionary in dict_list:
22         # iterate over dictionaries in input list
23         if not isinstance(dictionary, dict):
24             logger.info("Skipping non-dict details testcase [{}]".format(testcase))
25             continue
26         if keys == set(dictionary.keys()):
27             # check the dictionary structure
28             dicts.append(dictionary)
29     return dicts
30
31
32 def _get_results_from_list_of_dicts(list_of_dict_statuses, dict_indexes, expected_results=None):
33     test_results = {}
34     for test_status in list_of_dict_statuses:
35         status = test_status
36         for index in dict_indexes:
37             status = status[index]
38         if status in test_results:
39             test_results[status] += 1
40         else:
41             test_results[status] = 1
42
43     if expected_results is not None:
44         for expected_result in expected_results:
45             if expected_result not in test_results:
46                 test_results[expected_result] = 0
47
48     return test_results
49
50
51 def _convert_value(value):
52     return value if value != '' else 0
53
54
55 def _convert_duration(duration):
56     if (isinstance(duration, str) or isinstance(duration, unicode)) and ':' in duration:
57         hours, minutes, seconds = duration.split(":")
58         hours = _convert_value(hours)
59         minutes = _convert_value(minutes)
60         seconds = _convert_value(seconds)
61         int_duration = 3600 * int(hours) + 60 * int(minutes) + float(seconds)
62     else:
63         int_duration = duration
64     return int_duration
65
66
67 def modify_functest_tempest(testcase):
68     if modify_default_entry(testcase):
69         testcase_details = testcase['details']
70         testcase_tests = float(testcase_details['tests'])
71         testcase_failures = float(testcase_details['failures'])
72         if testcase_tests != 0:
73             testcase_details['success_percentage'] = 100 * (testcase_tests - testcase_failures) / testcase_tests
74         else:
75             testcase_details['success_percentage'] = 0
76         return True
77     else:
78         return False
79
80
81 def modify_functest_vims(testcase):
82     """
83     Structure:
84         details.sig_test.result.[{result}]
85         details.sig_test.duration
86         details.vIMS.duration
87         details.orchestrator.duration
88
89     Find data for these fields
90         -> details.sig_test.duration
91         -> details.sig_test.tests
92         -> details.sig_test.failures
93         -> details.sig_test.passed
94         -> details.sig_test.skipped
95         -> details.vIMS.duration
96         -> details.orchestrator.duration
97     """
98     testcase_details = testcase['details']
99     sig_test_results = _get_dicts_from_list(testcase, testcase_details['sig_test']['result'],
100                                             {'duration', 'result', 'name', 'error'})
101     if len(sig_test_results) < 1:
102         logger.info("No 'result' from 'sig_test' found in vIMS details, skipping")
103         return False
104     else:
105         test_results = _get_results_from_list_of_dicts(sig_test_results, ('result',), ('Passed', 'Skipped', 'Failed'))
106         passed = test_results['Passed']
107         skipped = test_results['Skipped']
108         failures = test_results['Failed']
109         all_tests = passed + skipped + failures
110         testcase['details'] = {
111             'sig_test': {
112                 'duration': testcase_details['sig_test']['duration'],
113                 'tests': all_tests,
114                 'failures': failures,
115                 'passed': passed,
116                 'skipped': skipped
117             },
118             'vIMS': {
119                 'duration': testcase_details['vIMS']['duration']
120             },
121             'orchestrator': {
122                 'duration': testcase_details['orchestrator']['duration']
123             }
124         }
125         return True
126
127
128 def modify_functest_onos(testcase):
129     """
130     Structure:
131         details.FUNCvirNet.duration
132         details.FUNCvirNet.status.[{Case result}]
133         details.FUNCvirNetL3.duration
134         details.FUNCvirNetL3.status.[{Case result}]
135
136     Find data for these fields
137         -> details.FUNCvirNet.duration
138         -> details.FUNCvirNet.tests
139         -> details.FUNCvirNet.failures
140         -> details.FUNCvirNetL3.duration
141         -> details.FUNCvirNetL3.tests
142         -> details.FUNCvirNetL3.failures
143     """
144     testcase_details = testcase['details']
145
146     funcvirnet_details = testcase_details['FUNCvirNet']['status']
147     funcvirnet_statuses = _get_dicts_from_list(testcase, funcvirnet_details, {'Case result', 'Case name:'})
148
149     funcvirnetl3_details = testcase_details['FUNCvirNetL3']['status']
150     funcvirnetl3_statuses = _get_dicts_from_list(testcase, funcvirnetl3_details, {'Case result', 'Case name:'})
151
152     if len(funcvirnet_statuses) < 0:
153         logger.info("No results found in 'FUNCvirNet' part of ONOS results")
154         return False
155     elif len(funcvirnetl3_statuses) < 0:
156         logger.info("No results found in 'FUNCvirNetL3' part of ONOS results")
157         return False
158     else:
159         funcvirnet_results = _get_results_from_list_of_dicts(funcvirnet_statuses,
160                                                              ('Case result',), ('PASS', 'FAIL'))
161         funcvirnetl3_results = _get_results_from_list_of_dicts(funcvirnetl3_statuses,
162                                                                ('Case result',), ('PASS', 'FAIL'))
163
164         funcvirnet_passed = funcvirnet_results['PASS']
165         funcvirnet_failed = funcvirnet_results['FAIL']
166         funcvirnet_all = funcvirnet_passed + funcvirnet_failed
167
168         funcvirnetl3_passed = funcvirnetl3_results['PASS']
169         funcvirnetl3_failed = funcvirnetl3_results['FAIL']
170         funcvirnetl3_all = funcvirnetl3_passed + funcvirnetl3_failed
171
172         testcase_details['FUNCvirNet'] = {
173             'duration': _convert_duration(testcase_details['FUNCvirNet']['duration']),
174             'tests': funcvirnet_all,
175             'failures': funcvirnet_failed
176         }
177
178         testcase_details['FUNCvirNetL3'] = {
179             'duration': _convert_duration(testcase_details['FUNCvirNetL3']['duration']),
180             'tests': funcvirnetl3_all,
181             'failures': funcvirnetl3_failed
182         }
183
184         return True
185
186
187 def modify_functest_rally(testcase):
188     """
189     Structure:
190         details.[{summary.duration}]
191         details.[{summary.nb success}]
192         details.[{summary.nb tests}]
193
194     Find data for these fields
195         -> details.duration
196         -> details.tests
197         -> details.success_percentage
198     """
199     summaries = _get_dicts_from_list(testcase, testcase['details'], {'summary'})
200
201     if len(summaries) != 1:
202         logger.info("Found zero or more than one 'summaries' in Rally details, skipping")
203         return False
204     else:
205         summary = summaries[0]['summary']
206         testcase['details'] = {
207             'duration': summary['duration'],
208             'tests': summary['nb tests'],
209             'success_percentage': summary['nb success']
210         }
211         return True
212
213
214 def modify_functest_odl(testcase):
215     """
216     Structure:
217         details.details.[{test_status.@status}]
218
219     Find data for these fields
220         -> details.tests
221         -> details.failures
222         -> details.success_percentage?
223     """
224     test_statuses = _get_dicts_from_list(testcase, testcase['details']['details'],
225                                          {'test_status', 'test_doc', 'test_name'})
226     if len(test_statuses) < 1:
227         logger.info("No 'test_status' found in ODL details, skipping")
228         return False
229     else:
230         test_results = _get_results_from_list_of_dicts(test_statuses, ('test_status', '@status'), ('PASS', 'FAIL'))
231
232         passed_tests = test_results['PASS']
233         failed_tests = test_results['FAIL']
234         all_tests = passed_tests + failed_tests
235
236         testcase['details'] = {
237             'tests': all_tests,
238             'failures': failed_tests,
239             'success_percentage': 100 * passed_tests / float(all_tests)
240         }
241         return True
242
243
244 def modify_default_entry(testcase):
245     """
246     Look for these and leave any of those:
247         details.duration
248         details.tests
249         details.failures
250
251     If none are present, then return False
252     """
253     found = False
254     testcase_details = testcase['details']
255     fields = ['duration', 'tests', 'failures']
256     if isinstance(testcase_details, dict):
257         for key, value in testcase_details.items():
258             if key in fields:
259                 found = True
260                 if key == 'duration':
261                     testcase_details[key] = _convert_duration(value)
262             else:
263                 del testcase_details[key]
264
265     return found
266
267
268 def _fix_date(date_string):
269     if isinstance(date_string, dict):
270         return date_string['$date']
271     else:
272         return date_string[:-3].replace(' ', 'T') + 'Z'
273
274
275 def verify_mongo_entry(testcase):
276     """
277     Mandatory fields:
278         installer
279         pod_name
280         version
281         case_name
282         date
283         project
284         details
285
286         these fields must be present and must NOT be None
287
288     Optional fields:
289         description
290
291         these fields will be preserved if the are NOT None
292     """
293     mandatory_fields = ['installer',
294                         'pod_name',
295                         'version',
296                         'case_name',
297                         'project_name',
298                         'details']
299     mandatory_fields_to_modify = {'creation_date': _fix_date}
300     if '_id' in testcase:
301         mongo_id = testcase['_id']
302     else:
303         mongo_id = None
304     optional_fields = ['description']
305     for key, value in testcase.items():
306         if key in mandatory_fields:
307             if value is None:
308                 # empty mandatory field, invalid input
309                 logger.info("Skipping testcase with mongo _id '{}' because the testcase was missing value"
310                             " for mandatory field '{}'".format(mongo_id, key))
311                 return False
312             else:
313                 mandatory_fields.remove(key)
314         elif key in mandatory_fields_to_modify:
315             if value is None:
316                 # empty mandatory field, invalid input
317                 logger.info("Skipping testcase with mongo _id '{}' because the testcase was missing value"
318                             " for mandatory field '{}'".format(mongo_id, key))
319                 return False
320             else:
321                 testcase[key] = mandatory_fields_to_modify[key](value)
322                 del mandatory_fields_to_modify[key]
323         elif key in optional_fields:
324             if value is None:
325                 # empty optional field, remove
326                 del testcase[key]
327             optional_fields.remove(key)
328         else:
329             # unknown field
330             del testcase[key]
331
332     if len(mandatory_fields) > 0:
333         # some mandatory fields are missing
334         logger.info("Skipping testcase with mongo _id '{}' because the testcase was missing"
335                     " mandatory field(s) '{}'".format(mongo_id, mandatory_fields))
336         return False
337     else:
338         return True
339
340
341 def modify_mongo_entry(testcase):
342     # 1. verify and identify the testcase
343     # 2. if modification is implemented, then use that
344     # 3. if not, try to use default
345     # 4. if 2 or 3 is successful, return True, otherwise return False
346     if verify_mongo_entry(testcase):
347         project = testcase['project_name']
348         case_name = testcase['case_name']
349         if project == 'functest':
350             if case_name == 'Rally':
351                 return modify_functest_rally(testcase)
352             elif case_name == 'ODL':
353                 return modify_functest_odl(testcase)
354             elif case_name == 'ONOS':
355                 return modify_functest_onos(testcase)
356             elif case_name == 'vIMS':
357                 return modify_functest_vims(testcase)
358             elif case_name == 'Tempest':
359                 return modify_functest_tempest(testcase)
360         return modify_default_entry(testcase)
361     else:
362         return False
363
364
365 def publish_mongo_data(output_destination):
366     tmp_filename = 'mongo-{}.log'.format(uuid.uuid4())
367     try:
368         subprocess.check_call(['mongoexport', '--db', 'test_results_collection', '-c', 'test_results', '--out',
369                                tmp_filename])
370         with open(tmp_filename) as fobj:
371             for mongo_json_line in fobj:
372                 test_result = json.loads(mongo_json_line)
373                 if modify_mongo_entry(test_result):
374                     shared_utils.publish_json(test_result, es_user, es_passwd, output_destination)
375     finally:
376         if os.path.exists(tmp_filename):
377             os.remove(tmp_filename)
378
379
380 def get_mongo_data(days):
381     past_time = datetime.datetime.today() - datetime.timedelta(days=days)
382     mongo_json_lines = subprocess.check_output(['mongoexport', '--db', 'test_results_collection', '-c', 'test_results',
383                                                 '--query', '{{"creation_date":{{$gt:"{}"}}}}'
384                                                .format(past_time)]).splitlines()
385
386     mongo_data = []
387     for mongo_json_line in mongo_json_lines:
388         test_result = json.loads(mongo_json_line)
389         if modify_mongo_entry(test_result):
390             # if the modification could be applied, append the modified result
391             mongo_data.append(test_result)
392     return mongo_data
393
394
395 def publish_difference(mongo_data, elastic_data, output_destination, es_user, es_passwd):
396     for elastic_entry in elastic_data:
397         if elastic_entry in mongo_data:
398             mongo_data.remove(elastic_entry)
399
400     logger.info('number of parsed test results: {}'.format(len(mongo_data)))
401
402     for parsed_test_result in mongo_data:
403         shared_utils.publish_json(parsed_test_result, es_user, es_passwd, output_destination)
404
405
406 if __name__ == '__main__':
407     parser = argparse.ArgumentParser(description='Modify and filter mongo json data for elasticsearch')
408     parser.add_argument('-od', '--output-destination',
409                         default='elasticsearch',
410                         choices=('elasticsearch', 'stdout'),
411                         help='defaults to elasticsearch')
412
413     parser.add_argument('-ml', '--merge-latest', default=0, type=int, metavar='N',
414                         help='get entries old at most N days from mongodb and'
415                              ' parse those that are not already in elasticsearch.'
416                              ' If not present, will get everything from mongodb, which is the default')
417
418     parser.add_argument('-e', '--elasticsearch-url', default='http://localhost:9200',
419                         help='the url of elasticsearch, defaults to http://localhost:9200')
420
421     parser.add_argument('-u', '--elasticsearch-username',
422                         help='the username for elasticsearch')
423
424     parser.add_argument('-p', '--elasticsearch-password',
425                         help='the password for elasticsearch')
426
427     parser.add_argument('-m', '--mongodb-url', default='http://localhost:8082',
428                         help='the url of mongodb, defaults to http://localhost:8082')
429
430     args = parser.parse_args()
431     base_elastic_url = urlparse.urljoin(args.elasticsearch_url, '/test_results/mongo2elastic')
432     output_destination = args.output_destination
433     days = args.merge_latest
434     es_user = args.elasticsearch_username
435     es_passwd = args.elasticsearch_password
436
437     if output_destination == 'elasticsearch':
438         output_destination = base_elastic_url
439
440     # parsed_test_results will be printed/sent to elasticsearch
441     if days == 0:
442         # TODO get everything from mongo
443         publish_mongo_data(output_destination)
444     elif days > 0:
445         body = '''{{
446     "query" : {{
447         "range" : {{
448             "creation_date" : {{
449                 "gte" : "now-{}d"
450             }}
451         }}
452     }}
453 }}'''.format(days)
454         elastic_data = shared_utils.get_elastic_data(base_elastic_url, es_user, es_passwd, body)
455         logger.info('number of hits in elasticsearch for now-{}d: {}'.format(days, len(elastic_data)))
456         mongo_data = get_mongo_data(days)
457         publish_difference(mongo_data, elastic_data, output_destination, es_user, es_passwd)
458     else:
459         raise Exception('Update must be non-negative')