summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/ci/check_grafana_dashboards.py
blob: d37337b404ed4c38f6463798c8d64bfb48b4d111 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# -*- coding: utf-8 -*-
# pylint: disable=F0401
"""
This script does:
* Scan through Angular html templates and extract <cd-grafana> tags
* Check if every tag has a corresponding Grafana dashboard by `uid`

Usage:
    python <script> <angular_app_dir> <grafana_dashboard_dir>

e.g.
    cd /ceph/src/pybind/mgr/dashboard
    python ci/<script> frontend/src/app /ceph/monitoring/ceph-mixin/dashboards_out
"""
import argparse
import codecs
import copy
import json
import os
from html.parser import HTMLParser


class TemplateParser(HTMLParser):

    def __init__(self, _file, search_tag):
        super().__init__()
        self.search_tag = search_tag
        self.file = _file
        self.parsed_data = []

    def parse(self):
        with codecs.open(self.file, encoding='UTF-8') as f:
            self.feed(f.read())

    def handle_starttag(self, tag, attrs):
        if tag != self.search_tag:
            return
        tag_data = {
            'file': self.file,
            'attrs': dict(attrs),
            'line': self.getpos()[0]
        }
        self.parsed_data.append(tag_data)

    def error(self, message):
        error_msg = 'fail to parse file {} (@{}): {}'.\
            format(self.file, self.getpos(), message)
        exit(error_msg)


def get_files(base_dir, file_ext):
    result = []
    for root, _, files in os.walk(base_dir):
        for _file in files:
            if _file.endswith('.{}'.format(file_ext)):
                result.append(os.path.join(root, _file))
    return result


def get_tags(base_dir, tag='cd-grafana'):
    templates = get_files(base_dir, 'html')
    tags = []
    for templ in templates:
        parser = TemplateParser(templ, tag)
        parser.parse()
        if parser.parsed_data:
            tags.extend(parser.parsed_data)
    return tags


def get_grafana_dashboards(base_dir):
    json_files = get_files(base_dir, 'json')
    dashboards = {}
    for json_file in json_files:
        try:
            with open(json_file) as f:
                dashboard_config = json.load(f)
                uid = dashboard_config.get('uid')
                # if it's not a grafana dashboard, skip checks
                # Fields in a dasbhoard:
                # https://grafana.com/docs/grafana/latest/dashboards/json-model/#json-fields
                expected_fields = [
                    'id', 'uid', 'title', 'tags', 'style', 'timezone', 'editable',
                    'hideControls', 'graphTooltip', 'panels', 'time', 'timepicker',
                    'templating', 'annotations', 'refresh', 'schemaVersion', 'version', 'links',
                ]
                not_a_dashboard = False
                for field in expected_fields:
                    if field not in dashboard_config:
                        not_a_dashboard = True
                        break
                if not_a_dashboard:
                    continue

                assert dashboard_config['id'] is None, \
                    "'id' not null: '{}'".format(dashboard_config['id'])

                assert 'timezone' not in dashboard_config or dashboard_config['timezone'] == '', \
                    ("'timezone' field must not be set to anything but an empty string or be "
                     "omitted completely")

                # Grafana dashboard checks
                title = dashboard_config['title']
                assert len(title) > 0, \
                    "Title not found in '{}'".format(json_file)
                assert len(dashboard_config.get('links', [])) == 0, \
                    "Links found in '{}'".format(json_file)
                if not uid:
                    continue
                if uid in dashboards:
                    # duplicated uids
                    error_msg = 'Duplicated UID {} found, already defined in {}'.\
                        format(uid, dashboards[uid]['file'])
                    exit(error_msg)

                dashboards[uid] = {
                    'file': json_file,
                    'title': title
                }
        except Exception as e:
            print(f"Error in file {json_file}")
            raise e
    return dashboards


def parse_args():
    long_desc = ('Check every <cd-grafana> component in Angular template has a'
                 ' mapped Grafana dashboard.')
    parser = argparse.ArgumentParser(description=long_desc)
    parser.add_argument('angular_app_dir', type=str,
                        help='Angular app base directory')
    parser.add_argument('grafana_dash_dir', type=str,
                        help='Directory contains Grafana dashboard JSON files')
    parser.add_argument('--verbose', action='store_true',
                        help='Display verbose mapping information.')
    return parser.parse_args()


def main():
    args = parse_args()
    tags = get_tags(args.angular_app_dir)
    grafana_dashboards = get_grafana_dashboards(args.grafana_dash_dir)
    verbose = args.verbose

    if not tags:
        error_msg = 'Can not find any cd-grafana component under {}'.\
            format(args.angular_app_dir)
        exit(error_msg)

    if verbose:
        print('Found mappings:')
    no_dashboard_tags = []
    for tag in tags:
        uid = tag['attrs']['uid']
        if uid not in grafana_dashboards:
            no_dashboard_tags.append(copy.copy(tag))
            continue
        if verbose:
            msg = '{} ({}:{}) \n\t-> {} ({})'.\
                format(uid, tag['file'], tag['line'],
                       grafana_dashboards[uid]['title'],
                       grafana_dashboards[uid]['file'])
            print(msg)

    if no_dashboard_tags:
        title = ('Checking Grafana dashboards UIDs: ERROR\n'
                 'Components that have no mapped Grafana dashboards:\n')
        lines = ('{} ({}:{})'.format(tag['attrs']['uid'],
                                     tag['file'],
                                     tag['line'])
                 for tag in no_dashboard_tags)
        error_msg = title + '\n'.join(lines)
        exit(error_msg)
    else:
        print('Checking Grafana dashboards UIDs: OK')


if __name__ == '__main__':
    main()