added ELK scripts for porting data from mongo to elasticsearch and managing kibana...
[releng.git] / utils / test / scripts / create_kibana_dashboards.py
1 #! /usr/bin/env python
2 import logging
3 import argparse
4 import shared_utils
5 import json
6 import urlparse
7
8 logger = logging.getLogger('create_kibana_dashboards')
9 logger.setLevel(logging.DEBUG)
10 file_handler = logging.FileHandler('/var/log/{}.log'.format(__name__))
11 file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
12 logger.addHandler(file_handler)
13
14 _installers = {'fuel', 'apex', 'compass', 'joid'}
15
16 # see class VisualizationState for details on format
17 _testcases = [
18     ('functest', 'Tempest',
19      [
20          {
21              "metrics": [
22                  {
23                      "type": "avg",
24                      "params": {
25                          "field": "details.duration"
26                      }
27                  }
28              ],
29              "type": "line",
30              "metadata": {
31                  "label": "Tempest duration",
32                  "test_family": "VIM"
33              }
34          },
35
36          {
37              "metrics": [
38                  {
39                      "type": "sum",
40                      "params": {
41                          "field": "details.tests"
42                      }
43                  },
44                  {
45                      "type": "sum",
46                      "params": {
47                          "field": "details.failures"
48                      }
49                  }
50              ],
51              "type": "histogram",
52              "metadata": {
53                  "label": "Tempest nr of tests/failures",
54                  "test_family": "VIM"
55              }
56          },
57
58          {
59              "metrics": [
60                  {
61                      "type": "avg",
62                      "params": {
63                          "field": "details.success_percentage"
64                      }
65                  }
66              ],
67              "type": "line",
68              "metadata": {
69                  "label": "Tempest success percentage",
70                  "test_family": "VIM"
71              }
72          }
73      ]
74      ),
75
76     ('functest', 'Rally',
77      [
78          {
79              "metrics": [
80                  {
81                      "type": "avg",
82                      "params": {
83                          "field": "details.duration"
84                      }
85                  }
86              ],
87              "type": "line",
88              "metadata": {
89                  "label": "Rally duration",
90                  "test_family": "VIM"
91              }
92          },
93
94          {
95              "metrics": [
96                  {
97                      "type": "avg",
98                      "params": {
99                          "field": "details.tests"
100                      }
101                  }
102              ],
103              "type": "histogram",
104              "metadata": {
105                  "label": "Rally nr of tests",
106                  "test_family": "VIM"
107              }
108          },
109
110          {
111              "metrics": [
112                  {
113                      "type": "avg",
114                      "params": {
115                          "field": "details.success_percentage"
116                      }
117                  }
118              ],
119              "type": "line",
120              "metadata": {
121                  "label": "Rally success percentage",
122                  "test_family": "VIM"
123              }
124          }
125      ]
126      ),
127
128     ('functest', 'vPing',
129      [
130          {
131              "metrics": [
132                  {
133                      "type": "avg",
134                      "params": {
135                          "field": "details.duration"
136                      }
137                  }
138              ],
139              "type": "line",
140              "metadata": {
141                  "label": "vPing duration",
142                  "test_family": "VIM"
143              }
144          }
145      ]
146      ),
147
148     ('functest', 'vPing_userdata',
149      [
150          {
151              "metrics": [
152                  {
153                      "type": "avg",
154                      "params": {
155                          "field": "details.duration"
156                      }
157                  }
158              ],
159              "type": "line",
160              "metadata": {
161                  "label": "vPing_userdata duration",
162                  "test_family": "VIM"
163              }
164          }
165      ]
166      ),
167
168     ('functest', 'ODL',
169      [
170          {
171              "metrics": [
172                  {
173                      "type": "sum",
174                      "params": {
175                          "field": "details.tests"
176                      }
177                  },
178                  {
179                      "type": "sum",
180                      "params": {
181                          "field": "details.failures"
182                      }
183                  }
184              ],
185              "type": "histogram",
186              "metadata": {
187                  "label": "ODL nr of tests/failures",
188                  "test_family": "Controller"
189              }
190          },
191
192          {
193              "metrics": [
194                  {
195                      "type": "avg",
196                      "params": {
197                          "field": "details.success_percentage"
198                      }
199                  }
200              ],
201              "type": "line",
202              "metadata": {
203                  "label": "ODL success percentage",
204                  "test_family": "Controller"
205              }
206          }
207      ]
208      ),
209
210     ('functest', 'ONOS',
211      [
212          {
213              "metrics": [
214                  {
215                      "type": "avg",
216                      "params": {
217                          "field": "details.FUNCvirNet.duration"
218                      }
219                  }
220              ],
221              "type": "line",
222              "metadata": {
223                  "label": "ONOS FUNCvirNet duration",
224                  "test_family": "Controller"
225              }
226          },
227
228          {
229              "metrics": [
230                  {
231                      "type": "sum",
232                      "params": {
233                          "field": "details.FUNCvirNet.tests"
234                      }
235                  },
236                  {
237                      "type": "sum",
238                      "params": {
239                          "field": "details.FUNCvirNet.failures"
240                      }
241                  }
242              ],
243              "type": "histogram",
244              "metadata": {
245                  "label": "ONOS FUNCvirNet nr of tests/failures",
246                  "test_family": "Controller"
247              }
248          },
249
250          {
251              "metrics": [
252                  {
253                      "type": "avg",
254                      "params": {
255                          "field": "details.FUNCvirNetL3.duration"
256                      }
257                  }
258              ],
259              "type": "line",
260              "metadata": {
261                  "label": "ONOS FUNCvirNetL3 duration",
262                  "test_family": "Controller"
263              }
264          },
265
266          {
267              "metrics": [
268                  {
269                      "type": "sum",
270                      "params": {
271                          "field": "details.FUNCvirNetL3.tests"
272                      }
273                  },
274                  {
275                      "type": "sum",
276                      "params": {
277                          "field": "details.FUNCvirNetL3.failures"
278                      }
279                  }
280              ],
281              "type": "histogram",
282              "metadata": {
283                  "label": "ONOS FUNCvirNetL3 nr of tests/failures",
284                  "test_family": "Controller"
285              }
286          }
287      ]
288      ),
289
290     ('functest', 'vIMS',
291      [
292          {
293              "metrics": [
294                  {
295                      "type": "sum",
296                      "params": {
297                          "field": "details.sig_test.tests"
298                      }
299                  },
300                  {
301                      "type": "sum",
302                      "params": {
303                          "field": "details.sig_test.failures"
304                      }
305                  },
306                  {
307                      "type": "sum",
308                      "params": {
309                          "field": "details.sig_test.passed"
310                      }
311                  },
312                  {
313                      "type": "sum",
314                      "params": {
315                          "field": "details.sig_test.skipped"
316                      }
317                  }
318              ],
319              "type": "histogram",
320              "metadata": {
321                  "label": "vIMS nr of tests/failures/passed/skipped",
322                  "test_family": "Features"
323              }
324          },
325
326          {
327              "metrics": [
328                  {
329                      "type": "avg",
330                      "params": {
331                          "field": "details.vIMS.duration"
332                      }
333                  },
334                  {
335                      "type": "avg",
336                      "params": {
337                          "field": "details.orchestrator.duration"
338                      }
339                  },
340                  {
341                      "type": "avg",
342                      "params": {
343                          "field": "details.sig_test.duration"
344                      }
345                  }
346              ],
347              "type": "histogram",
348              "metadata": {
349                  "label": "vIMS/ochestrator/test duration",
350                  "test_family": "Features"
351              }
352          }
353      ]
354      ),
355
356     ('promise', 'promise',
357      [
358          {
359              "metrics": [
360                  {
361                      "type": "avg",
362                      "params": {
363                          "field": "details.duration"
364                      }
365                  }
366              ],
367              "type": "line",
368              "metadata": {
369                  "label": "promise duration",
370                  "test_family": "Features"
371              }
372          },
373
374          {
375              "metrics": [
376                  {
377                      "type": "sum",
378                      "params": {
379                          "field": "details.tests"
380                      }
381                  },
382                  {
383                      "type": "sum",
384                      "params": {
385                          "field": "details.failures"
386                      }
387                  }
388              ],
389              "type": "histogram",
390              "metadata": {
391                  "label": "promise nr of tests/failures",
392                  "test_family": "Features"
393              }
394          }
395      ]
396      ),
397
398     ('doctor', 'doctor-notification',
399      [
400          {
401              "metrics": [
402                  {
403                      "type": "avg",
404                      "params": {
405                          "field": "details.duration"
406                      }
407                  }
408              ],
409              "type": "line",
410              "metadata": {
411                  "label": "doctor-notification duration",
412                  "test_family": "Features"
413              }
414          }
415      ]
416      )
417 ]
418
419
420 class KibanaDashboard(dict):
421     def __init__(self, project_name, case_name, installer, pod, versions, visualization_detail):
422         super(KibanaDashboard, self).__init__()
423         self.project_name = project_name
424         self.case_name = case_name
425         self.installer = installer
426         self.pod = pod
427         self.versions = versions
428         self.visualization_detail = visualization_detail
429         self._visualization_title = None
430         self._kibana_visualizations = []
431         self._kibana_dashboard = None
432         self._create_visualizations()
433         self._create()
434
435     def _create_visualizations(self):
436         for version in self.versions:
437             self._kibana_visualizations.append(KibanaVisualization(self.project_name,
438                                                                    self.case_name,
439                                                                    self.installer,
440                                                                    self.pod,
441                                                                    version,
442                                                                    self.visualization_detail))
443
444         self._visualization_title = self._kibana_visualizations[0].vis_state_title
445
446     def _publish_visualizations(self):
447         for visualization in self._kibana_visualizations:
448             url = urlparse.urljoin(base_elastic_url, '/.kibana/visualization/{}'.format(visualization.id))
449             logger.debug("publishing visualization '{}'".format(url))
450             shared_utils.publish_json(visualization, es_user, es_passwd, url)
451
452     def _construct_panels(self):
453         size_x = 6
454         size_y = 3
455         max_columns = 7
456         column = 1
457         row = 1
458         panel_index = 1
459         panels_json = []
460         for visualization in self._kibana_visualizations:
461             panels_json.append({
462                 "id": visualization.id,
463                 "type": 'visualization',
464                 "panelIndex": panel_index,
465                 "size_x": size_x,
466                 "size_y": size_y,
467                 "col": column,
468                 "row": row
469             })
470             panel_index += 1
471             column += size_x
472             if column > max_columns:
473                 column = 1
474                 row += size_y
475         return json.dumps(panels_json, separators=(',', ':'))
476
477     def _create(self):
478         self['title'] = '{} {} {} {} {}'.format(self.project_name,
479                                                 self.case_name,
480                                                 self.installer,
481                                                 self._visualization_title,
482                                                 self.pod)
483         self.id = self['title'].replace(' ', '-').replace('/', '-')
484
485         self['hits'] = 0
486         self['description'] = "Kibana dashboard for project_name '{}', case_name '{}', installer '{}', data '{}' and" \
487                               " pod '{}'".format(self.project_name,
488                                                  self.case_name,
489                                                  self.installer,
490                                                  self._visualization_title,
491                                                  self.pod)
492         self['panelsJSON'] = self._construct_panels()
493         self['optionsJSON'] = json.dumps({
494             "darkTheme": False
495         },
496             separators=(',', ':'))
497         self['uiStateJSON'] = "{}"
498         self['version'] = 1
499         self['timeRestore'] = False
500         self['kibanaSavedObjectMeta'] = {
501             'searchSourceJSON': json.dumps({
502                 "filter": [
503                     {
504                         "query": {
505                             "query_string": {
506                                 "query": "*",
507                                 "analyze_wildcard": True
508                             }
509                         }
510                     }
511                 ]
512             },
513                 separators=(',', ':'))
514         }
515         self['metadata'] = self.visualization_detail['metadata']
516
517     def _publish(self):
518         url = urlparse.urljoin(base_elastic_url, '/.kibana/dashboard/{}'.format(self.id))
519         logger.debug("publishing dashboard '{}'".format(url))
520         shared_utils.publish_json(self, es_user, es_passwd, url)
521
522     def publish(self):
523         self._publish_visualizations()
524         self._publish()
525
526
527 class KibanaSearchSourceJSON(dict):
528     """
529     "filter": [
530                     {"match": {"installer": {"query": installer, "type": "phrase"}}},
531                     {"match": {"project_name": {"query": project_name, "type": "phrase"}}},
532                     {"match": {"case_name": {"query": case_name, "type": "phrase"}}}
533                 ]
534     """
535
536     def __init__(self, project_name, case_name, installer, pod, version):
537         super(KibanaSearchSourceJSON, self).__init__()
538         self["filter"] = [
539             {"match": {"project_name": {"query": project_name, "type": "phrase"}}},
540             {"match": {"case_name": {"query": case_name, "type": "phrase"}}},
541             {"match": {"installer": {"query": installer, "type": "phrase"}}},
542             {"match": {"version": {"query": version, "type": "phrase"}}}
543         ]
544         if pod != 'all':
545             self["filter"].append({"match": {"pod_name": {"query": pod, "type": "phrase"}}})
546
547
548 class VisualizationState(dict):
549     def __init__(self, input_dict):
550         """
551         dict structure:
552             {
553             "metrics":
554                 [
555                     {
556                         "type": type,           # default sum
557                         "params": {
558                             "field": field      # mandatory, no default
559                     },
560                     {metric2}
561                 ],
562             "segments":
563                 [
564                     {
565                         "type": type,           # default date_histogram
566                         "params": {
567                             "field": field      # default creation_date
568                     },
569                     {segment2}
570                 ],
571             "type": type,                       # default area
572             "mode": mode,                       # default grouped for type 'histogram', stacked for other types
573             "metadata": {
574                     "label": "Tempest duration",# mandatory, no default
575                     "test_family": "VIM"        # mandatory, no default
576                 }
577             }
578
579         default modes:
580             type histogram: grouped
581             type area: stacked
582
583         :param input_dict:
584         :return:
585         """
586         super(VisualizationState, self).__init__()
587         metrics = input_dict['metrics']
588         segments = [] if 'segments' not in input_dict else input_dict['segments']
589
590         graph_type = 'area' if 'type' not in input_dict else input_dict['type']
591         self['type'] = graph_type
592
593         if 'mode' not in input_dict:
594             if graph_type == 'histogram':
595                 mode = 'grouped'
596             else:
597                 # default
598                 mode = 'stacked'
599         else:
600             mode = input_dict['mode']
601         self['params'] = {
602             "shareYAxis": True,
603             "addTooltip": True,
604             "addLegend": True,
605             "smoothLines": False,
606             "scale": "linear",
607             "interpolate": "linear",
608             "mode": mode,
609             "times": [],
610             "addTimeMarker": False,
611             "defaultYExtents": False,
612             "setYExtents": False,
613             "yAxis": {}
614         }
615
616         self['aggs'] = []
617
618         i = 1
619         for metric in metrics:
620             self['aggs'].append({
621                 "id": str(i),
622                 "type": 'sum' if 'type' not in metric else metric['type'],
623                 "schema": "metric",
624                 "params": {
625                     "field": metric['params']['field']
626                 }
627             })
628             i += 1
629
630         if len(segments) > 0:
631             for segment in segments:
632                 self['aggs'].append({
633                     "id": str(i),
634                     "type": 'date_histogram' if 'type' not in segment else segment['type'],
635                     "schema": "metric",
636                     "params": {
637                         "field": "creation_date" if ('params' not in segment or 'field' not in segment['params'])
638                         else segment['params']['field'],
639                         "interval": "auto",
640                         "customInterval": "2h",
641                         "min_doc_count": 1,
642                         "extended_bounds": {}
643                     }
644                 })
645                 i += 1
646         else:
647             self['aggs'].append({
648                 "id": str(i),
649                 "type": 'date_histogram',
650                 "schema": "segment",
651                 "params": {
652                     "field": "creation_date",
653                     "interval": "auto",
654                     "customInterval": "2h",
655                     "min_doc_count": 1,
656                     "extended_bounds": {}
657                 }
658             })
659
660         self['listeners'] = {}
661         self['title'] = ' '.join(['{} {}'.format(x['type'], x['params']['field']) for x in self['aggs']
662                                   if x['schema'] == 'metric'])
663
664
665 class KibanaVisualization(dict):
666     def __init__(self, project_name, case_name, installer, pod, version, detail):
667         """
668         We need two things
669         1. filter created from
670             project_name
671             case_name
672             installer
673             pod
674             version
675         2. visualization state
676             field for y axis (metric) with type (avg, sum, etc.)
677             field for x axis (segment) with type (date_histogram)
678
679         :return:
680         """
681         super(KibanaVisualization, self).__init__()
682         vis_state = VisualizationState(detail)
683         self.vis_state_title = vis_state['title']
684         self['title'] = '{} {} {} {} {} {}'.format(project_name,
685                                                    case_name,
686                                                    self.vis_state_title,
687                                                    installer,
688                                                    pod,
689                                                    version)
690         self.id = self['title'].replace(' ', '-').replace('/', '-')
691         self['visState'] = json.dumps(vis_state, separators=(',', ':'))
692         self['uiStateJSON'] = "{}"
693         self['description'] = "Kibana visualization for project_name '{}', case_name '{}', data '{}', installer '{}'," \
694                               " pod '{}' and version '{}'".format(project_name,
695                                                                   case_name,
696                                                                   self.vis_state_title,
697                                                                   installer,
698                                                                   pod,
699                                                                   version)
700         self['version'] = 1
701         self['kibanaSavedObjectMeta'] = {"searchSourceJSON": json.dumps(KibanaSearchSourceJSON(project_name,
702                                                                                                case_name,
703                                                                                                installer,
704                                                                                                pod,
705                                                                                                version),
706                                                                         separators=(',', ':'))}
707
708
709 def _get_pods_and_versions(project_name, case_name, installer):
710     query_json = json.JSONEncoder().encode({
711         "query": {
712             "bool": {
713                 "must": [
714                     {"match_all": {}}
715                 ],
716                 "filter": [
717                     {"match": {"installer": {"query": installer, "type": "phrase"}}},
718                     {"match": {"project_name": {"query": project_name, "type": "phrase"}}},
719                     {"match": {"case_name": {"query": case_name, "type": "phrase"}}}
720                 ]
721             }
722         }
723     })
724
725     elastic_data = shared_utils.get_elastic_data(urlparse.urljoin(base_elastic_url, '/test_results/mongo2elastic'),
726                                                  es_user, es_passwd, query_json)
727
728     pods_and_versions = {}
729
730     for data in elastic_data:
731         pod = data['pod_name']
732         if pod in pods_and_versions:
733             pods_and_versions[pod].add(data['version'])
734         else:
735             pods_and_versions[pod] = {data['version']}
736
737         if 'all' in pods_and_versions:
738             pods_and_versions['all'].add(data['version'])
739         else:
740             pods_and_versions['all'] = {data['version']}
741
742     return pods_and_versions
743
744
745 def construct_dashboards():
746     """
747     iterate over testcase and installer
748     1. get available pods for each testcase/installer pair
749     2. get available version for each testcase/installer/pod tuple
750     3. construct KibanaInput and append
751
752     :return: list of KibanaDashboards
753     """
754     kibana_dashboards = []
755     for project_name, case_name, visualization_details in _testcases:
756         for installer in _installers:
757             pods_and_versions = _get_pods_and_versions(project_name, case_name, installer)
758             for visualization_detail in visualization_details:
759                 for pod, versions in pods_and_versions.iteritems():
760                     kibana_dashboards.append(KibanaDashboard(project_name, case_name, installer, pod, versions,
761                                                              visualization_detail))
762     return kibana_dashboards
763
764
765 def generate_js_inputs(js_file_path, kibana_url, dashboards):
766     js_dict = {}
767     for dashboard in dashboards:
768         dashboard_meta = dashboard['metadata']
769         test_family = dashboard_meta['test_family']
770         test_label = dashboard_meta['label']
771
772         if test_family not in js_dict:
773             js_dict[test_family] = {}
774
775         js_test_family = js_dict[test_family]
776
777         if test_label not in js_test_family:
778             js_test_family[test_label] = {}
779
780         js_test_label = js_test_family[test_label]
781
782         if dashboard.installer not in js_test_label:
783             js_test_label[dashboard.installer] = {}
784
785         js_installer = js_test_label[dashboard.installer]
786         js_installer[dashboard.pod] = kibana_url + '#/dashboard/' + dashboard.id
787
788     with open(js_file_path, 'w+') as js_file_fdesc:
789         js_file_fdesc.write('var kibana_dashboard_links = ')
790         js_file_fdesc.write(str(js_dict).replace("u'", "'"))
791
792
793 if __name__ == '__main__':
794     parser = argparse.ArgumentParser(description='Create Kibana dashboards from data in elasticsearch')
795     parser.add_argument('-e', '--elasticsearch-url', default='http://localhost:9200',
796                         help='the url of elasticsearch, defaults to http://localhost:9200')
797     parser.add_argument('-js', '--generate_js_inputs', action='store_true',
798                         help='Use this argument to generate javascript inputs for kibana landing page')
799     parser.add_argument('--js_path', default='/usr/share/nginx/html/kibana_dashboards/conf.js',
800                         help='Path of javascript file with inputs for kibana landing page')
801     parser.add_argument('-k', '--kibana_url', default='https://testresults.opnfv.org/kibana/app/kibana',
802                         help='The url of kibana for javascript inputs')
803
804     parser.add_argument('-u', '--elasticsearch-username',
805                         help='the username for elasticsearch')
806
807     parser.add_argument('-p', '--elasticsearch-password',
808                         help='the password for elasticsearch')
809
810     args = parser.parse_args()
811     base_elastic_url = args.elasticsearch_url
812     generate_inputs = args.generate_js_inputs
813     input_file_path = args.js_path
814     kibana_url = args.kibana_url
815     es_user = args.elasticsearch_username
816     es_passwd = args.elasticsearch_password
817
818     dashboards = construct_dashboards()
819
820     for kibana_dashboard in dashboards:
821         kibana_dashboard.publish()
822
823     if generate_inputs:
824         generate_js_inputs(input_file_path, kibana_url, dashboards)