]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/ci/check_grafana_dashboards.py
import ceph quincy 17.2.6
[ceph.git] / ceph / src / pybind / mgr / dashboard / ci / check_grafana_dashboards.py
CommitLineData
92f5a8d4
TL
1# -*- coding: utf-8 -*-
2# pylint: disable=F0401
3"""
4This script does:
5* Scan through Angular html templates and extract <cd-grafana> tags
6* Check if every tag has a corresponding Grafana dashboard by `uid`
7
8Usage:
9 python <script> <angular_app_dir> <grafana_dashboard_dir>
10
11e.g.
12 cd /ceph/src/pybind/mgr/dashboard
20effc67 13 python ci/<script> frontend/src/app /ceph/monitoring/ceph-mixin/dashboards_out
92f5a8d4
TL
14"""
15import argparse
16import codecs
17import copy
18import json
19import os
f67539c2 20from html.parser import HTMLParser
92f5a8d4
TL
21
22
23class TemplateParser(HTMLParser):
24
25 def __init__(self, _file, search_tag):
f67539c2 26 super().__init__()
92f5a8d4
TL
27 self.search_tag = search_tag
28 self.file = _file
29 self.parsed_data = []
30
31 def parse(self):
32 with codecs.open(self.file, encoding='UTF-8') as f:
33 self.feed(f.read())
34
35 def handle_starttag(self, tag, attrs):
36 if tag != self.search_tag:
37 return
38 tag_data = {
39 'file': self.file,
40 'attrs': dict(attrs),
41 'line': self.getpos()[0]
42 }
43 self.parsed_data.append(tag_data)
44
45 def error(self, message):
46 error_msg = 'fail to parse file {} (@{}): {}'.\
47 format(self.file, self.getpos(), message)
48 exit(error_msg)
49
50
92f5a8d4
TL
51def get_files(base_dir, file_ext):
52 result = []
53 for root, _, files in os.walk(base_dir):
54 for _file in files:
55 if _file.endswith('.{}'.format(file_ext)):
56 result.append(os.path.join(root, _file))
57 return result
58
59
60def get_tags(base_dir, tag='cd-grafana'):
61 templates = get_files(base_dir, 'html')
62 tags = []
63 for templ in templates:
64 parser = TemplateParser(templ, tag)
65 parser.parse()
66 if parser.parsed_data:
67 tags.extend(parser.parsed_data)
68 return tags
69
70
71def get_grafana_dashboards(base_dir):
72 json_files = get_files(base_dir, 'json')
73 dashboards = {}
74 for json_file in json_files:
b3b6e05e
TL
75 try:
76 with open(json_file) as f:
77 dashboard_config = json.load(f)
78 uid = dashboard_config.get('uid')
20effc67
TL
79 # if it's not a grafana dashboard, skip checks
80 # Fields in a dasbhoard:
81 # https://grafana.com/docs/grafana/latest/dashboards/json-model/#json-fields
82 expected_fields = [
83 'id', 'uid', 'title', 'tags', 'style', 'timezone', 'editable',
84 'hideControls', 'graphTooltip', 'panels', 'time', 'timepicker',
85 'templating', 'annotations', 'refresh', 'schemaVersion', 'version', 'links',
86 ]
87 not_a_dashboard = False
88 for field in expected_fields:
89 if field not in dashboard_config:
90 not_a_dashboard = True
91 break
92 if not_a_dashboard:
93 continue
94
b3b6e05e
TL
95 assert dashboard_config['id'] is None, \
96 "'id' not null: '{}'".format(dashboard_config['id'])
97
522d829b
TL
98 assert 'timezone' not in dashboard_config or dashboard_config['timezone'] == '', \
99 ("'timezone' field must not be set to anything but an empty string or be "
100 "omitted completely")
101
b3b6e05e
TL
102 # Grafana dashboard checks
103 title = dashboard_config['title']
104 assert len(title) > 0, \
105 "Title not found in '{}'".format(json_file)
106 assert len(dashboard_config.get('links', [])) == 0, \
107 "Links found in '{}'".format(json_file)
108 if not uid:
109 continue
110 if uid in dashboards:
111 # duplicated uids
112 error_msg = 'Duplicated UID {} found, already defined in {}'.\
113 format(uid, dashboards[uid]['file'])
114 exit(error_msg)
115
116 dashboards[uid] = {
117 'file': json_file,
118 'title': title
119 }
120 except Exception as e:
121 print(f"Error in file {json_file}")
122 raise e
92f5a8d4
TL
123 return dashboards
124
125
126def parse_args():
127 long_desc = ('Check every <cd-grafana> component in Angular template has a'
128 ' mapped Grafana dashboard.')
129 parser = argparse.ArgumentParser(description=long_desc)
130 parser.add_argument('angular_app_dir', type=str,
131 help='Angular app base directory')
132 parser.add_argument('grafana_dash_dir', type=str,
133 help='Directory contains Grafana dashboard JSON files')
134 parser.add_argument('--verbose', action='store_true',
135 help='Display verbose mapping information.')
136 return parser.parse_args()
137
138
139def main():
140 args = parse_args()
141 tags = get_tags(args.angular_app_dir)
142 grafana_dashboards = get_grafana_dashboards(args.grafana_dash_dir)
143 verbose = args.verbose
144
145 if not tags:
146 error_msg = 'Can not find any cd-grafana component under {}'.\
147 format(args.angular_app_dir)
148 exit(error_msg)
149
150 if verbose:
f67539c2 151 print('Found mappings:')
92f5a8d4
TL
152 no_dashboard_tags = []
153 for tag in tags:
154 uid = tag['attrs']['uid']
155 if uid not in grafana_dashboards:
156 no_dashboard_tags.append(copy.copy(tag))
157 continue
158 if verbose:
159 msg = '{} ({}:{}) \n\t-> {} ({})'.\
160 format(uid, tag['file'], tag['line'],
161 grafana_dashboards[uid]['title'],
162 grafana_dashboards[uid]['file'])
f67539c2 163 print(msg)
92f5a8d4
TL
164
165 if no_dashboard_tags:
166 title = ('Checking Grafana dashboards UIDs: ERROR\n'
167 'Components that have no mapped Grafana dashboards:\n')
168 lines = ('{} ({}:{})'.format(tag['attrs']['uid'],
169 tag['file'],
170 tag['line'])
171 for tag in no_dashboard_tags)
172 error_msg = title + '\n'.join(lines)
173 exit(error_msg)
174 else:
f67539c2 175 print('Checking Grafana dashboards UIDs: OK')
92f5a8d4
TL
176
177
178if __name__ == '__main__':
179 main()