diff options
Diffstat (limited to 'utils/test/scripts/create_kibana_dashboards.py')
-rw-r--r-- | utils/test/scripts/create_kibana_dashboards.py | 824 |
1 files changed, 824 insertions, 0 deletions
diff --git a/utils/test/scripts/create_kibana_dashboards.py b/utils/test/scripts/create_kibana_dashboards.py new file mode 100644 index 000000000..252ce2138 --- /dev/null +++ b/utils/test/scripts/create_kibana_dashboards.py @@ -0,0 +1,824 @@ +#! /usr/bin/env python +import logging +import argparse +import shared_utils +import json +import urlparse + +logger = logging.getLogger('create_kibana_dashboards') +logger.setLevel(logging.DEBUG) +file_handler = logging.FileHandler('/var/log/{}.log'.format(__name__)) +file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s')) +logger.addHandler(file_handler) + +_installers = {'fuel', 'apex', 'compass', 'joid'} + +# see class VisualizationState for details on format +_testcases = [ + ('functest', 'Tempest', + [ + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.duration" + } + } + ], + "type": "line", + "metadata": { + "label": "Tempest duration", + "test_family": "VIM" + } + }, + + { + "metrics": [ + { + "type": "sum", + "params": { + "field": "details.tests" + } + }, + { + "type": "sum", + "params": { + "field": "details.failures" + } + } + ], + "type": "histogram", + "metadata": { + "label": "Tempest nr of tests/failures", + "test_family": "VIM" + } + }, + + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.success_percentage" + } + } + ], + "type": "line", + "metadata": { + "label": "Tempest success percentage", + "test_family": "VIM" + } + } + ] + ), + + ('functest', 'Rally', + [ + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.duration" + } + } + ], + "type": "line", + "metadata": { + "label": "Rally duration", + "test_family": "VIM" + } + }, + + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.tests" + } + } + ], + "type": "histogram", + "metadata": { + "label": "Rally nr of tests", + "test_family": "VIM" + } + }, + + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.success_percentage" + } + } + ], + "type": "line", + "metadata": { + "label": "Rally success percentage", + "test_family": "VIM" + } + } + ] + ), + + ('functest', 'vPing', + [ + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.duration" + } + } + ], + "type": "line", + "metadata": { + "label": "vPing duration", + "test_family": "VIM" + } + } + ] + ), + + ('functest', 'vPing_userdata', + [ + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.duration" + } + } + ], + "type": "line", + "metadata": { + "label": "vPing_userdata duration", + "test_family": "VIM" + } + } + ] + ), + + ('functest', 'ODL', + [ + { + "metrics": [ + { + "type": "sum", + "params": { + "field": "details.tests" + } + }, + { + "type": "sum", + "params": { + "field": "details.failures" + } + } + ], + "type": "histogram", + "metadata": { + "label": "ODL nr of tests/failures", + "test_family": "Controller" + } + }, + + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.success_percentage" + } + } + ], + "type": "line", + "metadata": { + "label": "ODL success percentage", + "test_family": "Controller" + } + } + ] + ), + + ('functest', 'ONOS', + [ + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.FUNCvirNet.duration" + } + } + ], + "type": "line", + "metadata": { + "label": "ONOS FUNCvirNet duration", + "test_family": "Controller" + } + }, + + { + "metrics": [ + { + "type": "sum", + "params": { + "field": "details.FUNCvirNet.tests" + } + }, + { + "type": "sum", + "params": { + "field": "details.FUNCvirNet.failures" + } + } + ], + "type": "histogram", + "metadata": { + "label": "ONOS FUNCvirNet nr of tests/failures", + "test_family": "Controller" + } + }, + + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.FUNCvirNetL3.duration" + } + } + ], + "type": "line", + "metadata": { + "label": "ONOS FUNCvirNetL3 duration", + "test_family": "Controller" + } + }, + + { + "metrics": [ + { + "type": "sum", + "params": { + "field": "details.FUNCvirNetL3.tests" + } + }, + { + "type": "sum", + "params": { + "field": "details.FUNCvirNetL3.failures" + } + } + ], + "type": "histogram", + "metadata": { + "label": "ONOS FUNCvirNetL3 nr of tests/failures", + "test_family": "Controller" + } + } + ] + ), + + ('functest', 'vIMS', + [ + { + "metrics": [ + { + "type": "sum", + "params": { + "field": "details.sig_test.tests" + } + }, + { + "type": "sum", + "params": { + "field": "details.sig_test.failures" + } + }, + { + "type": "sum", + "params": { + "field": "details.sig_test.passed" + } + }, + { + "type": "sum", + "params": { + "field": "details.sig_test.skipped" + } + } + ], + "type": "histogram", + "metadata": { + "label": "vIMS nr of tests/failures/passed/skipped", + "test_family": "Features" + } + }, + + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.vIMS.duration" + } + }, + { + "type": "avg", + "params": { + "field": "details.orchestrator.duration" + } + }, + { + "type": "avg", + "params": { + "field": "details.sig_test.duration" + } + } + ], + "type": "histogram", + "metadata": { + "label": "vIMS/ochestrator/test duration", + "test_family": "Features" + } + } + ] + ), + + ('promise', 'promise', + [ + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.duration" + } + } + ], + "type": "line", + "metadata": { + "label": "promise duration", + "test_family": "Features" + } + }, + + { + "metrics": [ + { + "type": "sum", + "params": { + "field": "details.tests" + } + }, + { + "type": "sum", + "params": { + "field": "details.failures" + } + } + ], + "type": "histogram", + "metadata": { + "label": "promise nr of tests/failures", + "test_family": "Features" + } + } + ] + ), + + ('doctor', 'doctor-notification', + [ + { + "metrics": [ + { + "type": "avg", + "params": { + "field": "details.duration" + } + } + ], + "type": "line", + "metadata": { + "label": "doctor-notification duration", + "test_family": "Features" + } + } + ] + ) +] + + +class KibanaDashboard(dict): + def __init__(self, project_name, case_name, installer, pod, versions, visualization_detail): + super(KibanaDashboard, self).__init__() + self.project_name = project_name + self.case_name = case_name + self.installer = installer + self.pod = pod + self.versions = versions + self.visualization_detail = visualization_detail + self._visualization_title = None + self._kibana_visualizations = [] + self._kibana_dashboard = None + self._create_visualizations() + self._create() + + def _create_visualizations(self): + for version in self.versions: + self._kibana_visualizations.append(KibanaVisualization(self.project_name, + self.case_name, + self.installer, + self.pod, + version, + self.visualization_detail)) + + self._visualization_title = self._kibana_visualizations[0].vis_state_title + + def _publish_visualizations(self): + for visualization in self._kibana_visualizations: + url = urlparse.urljoin(base_elastic_url, '/.kibana/visualization/{}'.format(visualization.id)) + logger.debug("publishing visualization '{}'".format(url)) + shared_utils.publish_json(visualization, es_user, es_passwd, url) + + def _construct_panels(self): + size_x = 6 + size_y = 3 + max_columns = 7 + column = 1 + row = 1 + panel_index = 1 + panels_json = [] + for visualization in self._kibana_visualizations: + panels_json.append({ + "id": visualization.id, + "type": 'visualization', + "panelIndex": panel_index, + "size_x": size_x, + "size_y": size_y, + "col": column, + "row": row + }) + panel_index += 1 + column += size_x + if column > max_columns: + column = 1 + row += size_y + return json.dumps(panels_json, separators=(',', ':')) + + def _create(self): + self['title'] = '{} {} {} {} {}'.format(self.project_name, + self.case_name, + self.installer, + self._visualization_title, + self.pod) + self.id = self['title'].replace(' ', '-').replace('/', '-') + + self['hits'] = 0 + self['description'] = "Kibana dashboard for project_name '{}', case_name '{}', installer '{}', data '{}' and" \ + " pod '{}'".format(self.project_name, + self.case_name, + self.installer, + self._visualization_title, + self.pod) + self['panelsJSON'] = self._construct_panels() + self['optionsJSON'] = json.dumps({ + "darkTheme": False + }, + separators=(',', ':')) + self['uiStateJSON'] = "{}" + self['version'] = 1 + self['timeRestore'] = False + self['kibanaSavedObjectMeta'] = { + 'searchSourceJSON': json.dumps({ + "filter": [ + { + "query": { + "query_string": { + "query": "*", + "analyze_wildcard": True + } + } + } + ] + }, + separators=(',', ':')) + } + self['metadata'] = self.visualization_detail['metadata'] + + def _publish(self): + url = urlparse.urljoin(base_elastic_url, '/.kibana/dashboard/{}'.format(self.id)) + logger.debug("publishing dashboard '{}'".format(url)) + shared_utils.publish_json(self, es_user, es_passwd, url) + + def publish(self): + self._publish_visualizations() + self._publish() + + +class KibanaSearchSourceJSON(dict): + """ + "filter": [ + {"match": {"installer": {"query": installer, "type": "phrase"}}}, + {"match": {"project_name": {"query": project_name, "type": "phrase"}}}, + {"match": {"case_name": {"query": case_name, "type": "phrase"}}} + ] + """ + + def __init__(self, project_name, case_name, installer, pod, version): + super(KibanaSearchSourceJSON, self).__init__() + self["filter"] = [ + {"match": {"project_name": {"query": project_name, "type": "phrase"}}}, + {"match": {"case_name": {"query": case_name, "type": "phrase"}}}, + {"match": {"installer": {"query": installer, "type": "phrase"}}}, + {"match": {"version": {"query": version, "type": "phrase"}}} + ] + if pod != 'all': + self["filter"].append({"match": {"pod_name": {"query": pod, "type": "phrase"}}}) + + +class VisualizationState(dict): + def __init__(self, input_dict): + """ + dict structure: + { + "metrics": + [ + { + "type": type, # default sum + "params": { + "field": field # mandatory, no default + }, + {metric2} + ], + "segments": + [ + { + "type": type, # default date_histogram + "params": { + "field": field # default creation_date + }, + {segment2} + ], + "type": type, # default area + "mode": mode, # default grouped for type 'histogram', stacked for other types + "metadata": { + "label": "Tempest duration",# mandatory, no default + "test_family": "VIM" # mandatory, no default + } + } + + default modes: + type histogram: grouped + type area: stacked + + :param input_dict: + :return: + """ + super(VisualizationState, self).__init__() + metrics = input_dict['metrics'] + segments = [] if 'segments' not in input_dict else input_dict['segments'] + + graph_type = 'area' if 'type' not in input_dict else input_dict['type'] + self['type'] = graph_type + + if 'mode' not in input_dict: + if graph_type == 'histogram': + mode = 'grouped' + else: + # default + mode = 'stacked' + else: + mode = input_dict['mode'] + self['params'] = { + "shareYAxis": True, + "addTooltip": True, + "addLegend": True, + "smoothLines": False, + "scale": "linear", + "interpolate": "linear", + "mode": mode, + "times": [], + "addTimeMarker": False, + "defaultYExtents": False, + "setYExtents": False, + "yAxis": {} + } + + self['aggs'] = [] + + i = 1 + for metric in metrics: + self['aggs'].append({ + "id": str(i), + "type": 'sum' if 'type' not in metric else metric['type'], + "schema": "metric", + "params": { + "field": metric['params']['field'] + } + }) + i += 1 + + if len(segments) > 0: + for segment in segments: + self['aggs'].append({ + "id": str(i), + "type": 'date_histogram' if 'type' not in segment else segment['type'], + "schema": "metric", + "params": { + "field": "creation_date" if ('params' not in segment or 'field' not in segment['params']) + else segment['params']['field'], + "interval": "auto", + "customInterval": "2h", + "min_doc_count": 1, + "extended_bounds": {} + } + }) + i += 1 + else: + self['aggs'].append({ + "id": str(i), + "type": 'date_histogram', + "schema": "segment", + "params": { + "field": "creation_date", + "interval": "auto", + "customInterval": "2h", + "min_doc_count": 1, + "extended_bounds": {} + } + }) + + self['listeners'] = {} + self['title'] = ' '.join(['{} {}'.format(x['type'], x['params']['field']) for x in self['aggs'] + if x['schema'] == 'metric']) + + +class KibanaVisualization(dict): + def __init__(self, project_name, case_name, installer, pod, version, detail): + """ + We need two things + 1. filter created from + project_name + case_name + installer + pod + version + 2. visualization state + field for y axis (metric) with type (avg, sum, etc.) + field for x axis (segment) with type (date_histogram) + + :return: + """ + super(KibanaVisualization, self).__init__() + vis_state = VisualizationState(detail) + self.vis_state_title = vis_state['title'] + self['title'] = '{} {} {} {} {} {}'.format(project_name, + case_name, + self.vis_state_title, + installer, + pod, + version) + self.id = self['title'].replace(' ', '-').replace('/', '-') + self['visState'] = json.dumps(vis_state, separators=(',', ':')) + self['uiStateJSON'] = "{}" + self['description'] = "Kibana visualization for project_name '{}', case_name '{}', data '{}', installer '{}'," \ + " pod '{}' and version '{}'".format(project_name, + case_name, + self.vis_state_title, + installer, + pod, + version) + self['version'] = 1 + self['kibanaSavedObjectMeta'] = {"searchSourceJSON": json.dumps(KibanaSearchSourceJSON(project_name, + case_name, + installer, + pod, + version), + separators=(',', ':'))} + + +def _get_pods_and_versions(project_name, case_name, installer): + query_json = json.JSONEncoder().encode({ + "query": { + "bool": { + "must": [ + {"match_all": {}} + ], + "filter": [ + {"match": {"installer": {"query": installer, "type": "phrase"}}}, + {"match": {"project_name": {"query": project_name, "type": "phrase"}}}, + {"match": {"case_name": {"query": case_name, "type": "phrase"}}} + ] + } + } + }) + + elastic_data = shared_utils.get_elastic_data(urlparse.urljoin(base_elastic_url, '/test_results/mongo2elastic'), + es_user, es_passwd, query_json) + + pods_and_versions = {} + + for data in elastic_data: + pod = data['pod_name'] + if pod in pods_and_versions: + pods_and_versions[pod].add(data['version']) + else: + pods_and_versions[pod] = {data['version']} + + if 'all' in pods_and_versions: + pods_and_versions['all'].add(data['version']) + else: + pods_and_versions['all'] = {data['version']} + + return pods_and_versions + + +def construct_dashboards(): + """ + iterate over testcase and installer + 1. get available pods for each testcase/installer pair + 2. get available version for each testcase/installer/pod tuple + 3. construct KibanaInput and append + + :return: list of KibanaDashboards + """ + kibana_dashboards = [] + for project_name, case_name, visualization_details in _testcases: + for installer in _installers: + pods_and_versions = _get_pods_and_versions(project_name, case_name, installer) + for visualization_detail in visualization_details: + for pod, versions in pods_and_versions.iteritems(): + kibana_dashboards.append(KibanaDashboard(project_name, case_name, installer, pod, versions, + visualization_detail)) + return kibana_dashboards + + +def generate_js_inputs(js_file_path, kibana_url, dashboards): + js_dict = {} + for dashboard in dashboards: + dashboard_meta = dashboard['metadata'] + test_family = dashboard_meta['test_family'] + test_label = dashboard_meta['label'] + + if test_family not in js_dict: + js_dict[test_family] = {} + + js_test_family = js_dict[test_family] + + if test_label not in js_test_family: + js_test_family[test_label] = {} + + js_test_label = js_test_family[test_label] + + if dashboard.installer not in js_test_label: + js_test_label[dashboard.installer] = {} + + js_installer = js_test_label[dashboard.installer] + js_installer[dashboard.pod] = kibana_url + '#/dashboard/' + dashboard.id + + with open(js_file_path, 'w+') as js_file_fdesc: + js_file_fdesc.write('var kibana_dashboard_links = ') + js_file_fdesc.write(str(js_dict).replace("u'", "'")) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Create Kibana dashboards from data in elasticsearch') + parser.add_argument('-e', '--elasticsearch-url', default='http://localhost:9200', + help='the url of elasticsearch, defaults to http://localhost:9200') + parser.add_argument('-js', '--generate_js_inputs', action='store_true', + help='Use this argument to generate javascript inputs for kibana landing page') + parser.add_argument('--js_path', default='/usr/share/nginx/html/kibana_dashboards/conf.js', + help='Path of javascript file with inputs for kibana landing page') + parser.add_argument('-k', '--kibana_url', default='https://testresults.opnfv.org/kibana/app/kibana', + help='The url of kibana for javascript inputs') + + parser.add_argument('-u', '--elasticsearch-username', + help='the username for elasticsearch') + + parser.add_argument('-p', '--elasticsearch-password', + help='the password for elasticsearch') + + args = parser.parse_args() + base_elastic_url = args.elasticsearch_url + generate_inputs = args.generate_js_inputs + input_file_path = args.js_path + kibana_url = args.kibana_url + es_user = args.elasticsearch_username + es_passwd = args.elasticsearch_password + + dashboards = construct_dashboards() + + for kibana_dashboard in dashboards: + kibana_dashboard.publish() + + if generate_inputs: + generate_js_inputs(input_file_path, kibana_url, dashboards) |