]>
Commit | Line | Data |
---|---|---|
92f5a8d4 TL |
1 | # -*- coding: utf-8 -*- |
2 | # pylint: disable=F0401 | |
3 | """ | |
4 | This 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 | ||
8 | Usage: | |
9 | python <script> <angular_app_dir> <grafana_dashboard_dir> | |
10 | ||
11 | e.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 | """ |
15 | import argparse | |
16 | import codecs | |
17 | import copy | |
18 | import json | |
19 | import os | |
f67539c2 | 20 | from html.parser import HTMLParser |
92f5a8d4 TL |
21 | |
22 | ||
23 | class 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 |
51 | def 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 | ||
60 | def 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 | ||
71 | def 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 | ||
126 | def 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 | ||
139 | def 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 | ||
178 | if __name__ == '__main__': | |
179 | main() |