├── LICENSE ├── Makefile.example ├── README.md ├── config.yml.example ├── generate.py └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright since 2015 Showmax s.r.o. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile.example: -------------------------------------------------------------------------------- 1 | # This is example Makefile for projects using grafana-dashboards-generator 2 | # Please check README.md for additional information 3 | 4 | all: update-submodule genconfig deploy 5 | 6 | genconfig: grafana-dashboards-generator config.yml clean dashboards 7 | ./grafana-dashboards-generator/generate.py -c config.yml -d dashboards 8 | 9 | dashboards: 10 | @mkdir -p dashboards 11 | 12 | update-submodule: 13 | git submodule update --init 14 | 15 | deploy: genconfig 16 | @echo Here can be you deployment strategy 17 | 18 | clean: 19 | rm -rf dashboards 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grafana dashboards generator 2 | 3 | We use this project at [Showmax](http://tech.showmax.com) to generate JSON definitions of Grafana dashboards. Main motivation for the existence of this tool is to 4 | 5 | * have a central place for keeping all dashboards in human readable code 6 | * track changes with git 7 | * be able to deploy dashboards to [Grafana](http://grafana.org) started in fresh container without need for persisting changes made into the container. 8 | 9 | We use the awesome [Prometheus](http://www.prometheus.io) for storing our metrics. 10 | 11 | ## Using the generator 12 | 13 | We are using the generator as a git submodule in our projects, which hold the actual configuration files. The typical configuration project contains: 14 | 15 | - ``config.yml`` with dashboards definition 16 | - ``Makefile`` for generating configuration and deploying generated dashboards to Grafana 17 | 18 | Then the day-to-day use looks like: 19 | 20 | 1. edit ``config.yml`` 21 | 1. run ``make genconfig`` 22 | 1. if everything is happy, commit updated ``config.yml`` to git 23 | 1. run ``git push`` 24 | 1. run ``make deploy`` 25 | 26 | ### Preparing configuration project 27 | 28 | To start using `grafana-dashboards-generator` you should create a new git repository for holding your configuration. The process of starting a new project would look something like 29 | 30 | ```bash 31 | mkdir company-awesome-dashboards && cd company-awesome-dashboards 32 | git init 33 | git submodule add git@github.com:ShowMax/grafana-dashboards-generator.git 34 | cp grafana-dashboards-generator/Makefile.example Makefile 35 | cp grafana-dashboards-generator/config.yml.example config.yml 36 | ``` 37 | 38 | You are now ready to edit ``Makefile`` to configure your ``deploy`` target. As well as edit ``config.yml`` to configure your awesome dashboards. 39 | 40 | ### Deploying to Grafana 41 | 42 | We have omitted deploy step from the `Makefile` as it will be environment specific. In general you need to POST generated files (which are located in `dashboards` directory) to Grafana. We have the following configuration in our Grafana `Dockerfile`: 43 | 44 | ```bash 45 | export GF_DASHBOARDS_JSON_ENABLED=true 46 | export GF_DASHBOARDS_JSON_PATH=/opt/showmax/grafana-dashboards/dashboards 47 | ``` 48 | 49 | And then just restart Grafana, so it reads new configuration. 50 | 51 | ## TODO 52 | 53 | List of things we would like to do see in the future versions: 54 | 55 | * better error reporting if invalid configuration is passed 56 | * graph_overrides to dashboard section and maybe something similar to `seriesOverride` as well 57 | ``` 58 | graph_overrides: 59 | height: 500px 60 | will "inject" height for all graphs in this dashboard regardless of 61 | graph template 62 | ``` 63 | * `expvars` - allow list and instantiate expression for all values 64 | * better inheritance of dashboard sections - inherit all rows and change/discard just few of them, inherit all expvars and override/discard just some of them, ditto for tags 65 | -------------------------------------------------------------------------------- /config.yml.example: -------------------------------------------------------------------------------- 1 | # POSSIBLE PROBLEMS: 2 | # 3 | # If the script is complaining about missing attributes in object like "Row", 4 | # check that you've inherited from a template defining all basic attributes :-) 5 | # 6 | # NOTE: expvars should be always specified in full (or: TODO: better expvars inheritance) (dtto for dashboardLinks and tags) 7 | # NOTE: yaml doesn't like {{ }} ... so e.g. {{exported_instance}} has to be in dquotes 8 | # NOTE: "Row has no collapse attribute" usualy means you've forgot to inherit basic graph_template_lines 9 | # NOTE: target['expr'] = target_data['expression'] % expvars 10 | # TypeError: string indices must be integers, not unicode 11 | # target items have to be dictionaries (e.g. "- expression: sum(..." ; you've used "- sum(...)" 12 | 13 | dashboards: 14 | template: 15 | instantiate: false # do not create this dashboard it's an abstract template! 16 | editable: true 17 | timezone: utc 18 | hideControls: false 19 | style: dark 20 | sharedCrosshair: true 21 | refresh: 15m 22 | tags: [] 23 | time: 24 | from: now-2d 25 | to: now 26 | now: true 27 | refresh_intervals: 28 | - 15s 29 | - 1m 30 | - 5m 31 | - 15m 32 | - 1h 33 | - 6h 34 | - 12h 35 | - 24h 36 | - 2d 37 | - 7d 38 | - 30d 39 | time_options: 40 | - 5m 41 | - 15m 42 | - 1h 43 | - 6h 44 | - 12h 45 | - 24h 46 | - 2d 47 | - 3d 48 | - 7d 49 | - 30d 50 | expvars: 51 | net_device: eth0 52 | folder: General 53 | CoreOS: 54 | instantiate: true # mandatory true! otherwise it'll inherit false from template... 55 | title: Core OS 56 | originalTitle: Core OS 57 | inherits: template 58 | tags: [] 59 | rows: 60 | - load1 61 | - usedMemoryExBuffersAndCache 62 | - networkReceive 63 | - networkTransmit 64 | - procs_running 65 | - diskRead 66 | - diskWrite 67 | - diskSpace 68 | - diskInodes 69 | expvars: 70 | net_device: eth0 71 | filesystem: / 72 | instance_selector: ^coreos\d+[a-z]\.example\.com$ 73 | templating: 74 | - node_load1_exported_instance 75 | templating_regexps: 76 | node_load1_exported_instance: instance_selector 77 | rows: 78 | graph_template_lines: 79 | # do NOT delete variables from here, generate.py assumes they exist here 80 | type: graph 81 | bars: false 82 | collapse: false 83 | datasource: null 84 | editable: true 85 | error: false 86 | fill: 0 87 | grid_leftLogBase: 1 88 | grid_leftMax: null 89 | grid_leftMin: null 90 | grid_rightLogBase: 1 91 | grid_rightMax: null 92 | grid_rightMin: null 93 | grid_threshold1Color: "rgba(216, 200, 27, 0.27)" 94 | grid_threshold1: null 95 | grid_threshold2Color: "rgba(234, 112, 112, 0.22)" 96 | grid_threshold2: null 97 | height: 250px 98 | intervalFactor: 1 99 | leftYAxisLabel: data 100 | legend_alignAsTable: false 101 | legend_avg: true 102 | legend_current: true 103 | legend_format: "{{exported_instance}}" 104 | legend_max: true 105 | legend_min: false 106 | legend_show: true 107 | legend_total: false 108 | legend_values: true 109 | tooltip_shared: true 110 | lines: true 111 | lineWidth: 1 112 | nullPointMode: "null as zero" 113 | percentage: false 114 | pointradius: 2 115 | points: false 116 | renderer: png # flot == in browser, png == on server 117 | rightYAxisLabel: "" 118 | span: 12 119 | stack: false 120 | steppedLine: false 121 | timeFrom: null 122 | x-axis: true 123 | y-axis: true 124 | y_formats: [short,short] 125 | load1: 126 | title: load1 127 | inherits: graph_template_lines 128 | leftYAxisLabel: LoadAVG 129 | targets: 130 | - expression: node_load1{exported_instance=~"$node_load1_exported_instance"} 131 | procs_running: 132 | title: Processes 133 | inherits: graph_template_lines 134 | leftYAxisLabel: "# of processes" 135 | targets: 136 | - expression: node_procs_running{exported_instance=~"$node_load1_exported_instance"} 137 | usedMemoryExBuffersAndCache: 138 | title: Memory used excluding buffers and cache 139 | inherits: graph_template_lines 140 | leftYAxisLabel: bytes 141 | y_formats: [bytes,bytes] 142 | targets: 143 | - expression: node_memory_MemTotal{exported_instance=~"$node_load1_exported_instance"}-(node_memory_MemFree{exported_instance=~"$node_load1_exported_instance"}+node_memory_Cached{exported_instance=~"$node_load1_exported_instance"}+node_memory_Buffers{exported_instance=~"$node_load1_exported_instance"}) 144 | diskRead: 145 | title: disk reads (ms) 146 | inherits: graph_template_lines 147 | leftYAxisLabel: ms 148 | y_formats: [ms,ms] 149 | targets: 150 | - expression: irate(node_disk_read_time_ms{exported_instance=~"$node_load1_exported_instance",device="sda"}[5m]) 151 | diskWrite: 152 | title: disk writes (ms) 153 | inherits: graph_template_lines 154 | leftYAxisLabel: ms 155 | y_formats: [ms,ms] 156 | targets: 157 | - expression: irate(node_disk_write_time_ms{exported_instance=~"$node_load1_exported_instance",device="sda"}[5m]) 158 | diskSpace: 159 | title: Filesystem free space 160 | inherits: graph_template_lines 161 | leftYAxisLabel: percentage 162 | tooltip_shared: false 163 | percentage: true 164 | grid_leftMax: 100 165 | grid_leftMin: 0 166 | y_formats: [percent, percent] 167 | fill: 0 168 | legend_max: false 169 | legend_min: true 170 | legend_format: "{{filesystem}}" 171 | targets: 172 | - expression: (node_filesystem_avail{exported_instance=~"$node_load1_exported_instance", filesystem=~"%(filesystem)s"} / node_filesystem_size{exported_instance=~"$node_load1_exported_instance", filesystem=~"%(filesystem)s"}) * 100 173 | diskInodes: 174 | title: Filesystem free Inodes 175 | inherits: graph_template_lines 176 | leftYAxisLabel: percentage 177 | tooltip_shared: false 178 | percentage: true 179 | grid_leftMax: 100 180 | y_formats: [percent, percent] 181 | fill: 1 182 | legend_max: false 183 | legend_min: true 184 | legend_format: "{{filesystem}}" 185 | targets: 186 | - expression: (node_filesystem_files_free{exported_instance=~"$node_load1_exported_instance", filesystem=~"%(filesystem)s"} / node_filesystem_files{exported_instance=~"$node_load1_exported_instance", filesystem=~"%(filesystem)s"}) * 100 187 | networkTransmit: 188 | title: Network Transmit bps 189 | inherits: graph_template_lines 190 | leftYAxisLabel: bps 191 | y_formats: [bps, bps] 192 | targets: 193 | - expression: irate(node_network_transmit_bytes{exported_instance=~"$node_load1_exported_instance",device="%(net_device)s"}[5m])*8 194 | networkReceive: 195 | title: Network Receive bps 196 | leftYAxisLabel: bps 197 | inherits: graph_template_lines 198 | y_formats: [bps, bps] 199 | targets: 200 | - expression: irate(node_network_receive_bytes{exported_instance=~"$node_load1_exported_instance",device="%(net_device)s"}[5m])*8 201 | templating: 202 | templating_template: 203 | allValue: null 204 | current: {} 205 | datasource: null 206 | hide: 0 207 | includeAll: true 208 | label: null 209 | multi: true 210 | options: [] 211 | refresh: 1 212 | sort: 1 213 | tagValuesQuery: "" 214 | tags: [] 215 | tagsQuery: "" 216 | type: "query" 217 | useTags: false 218 | node_load1_exported_instance: 219 | inherits: templating_template 220 | metric: node_load1 221 | label: exported_instance 222 | dashboardLinks: 223 | template: 224 | type: link 225 | icon: external link 226 | tags: [] 227 | targetBlank: true 228 | url: http://www.showmax.com/ 229 | -------------------------------------------------------------------------------- /generate.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | """ 3 | """ 4 | 5 | from __future__ import unicode_literals 6 | 7 | import os 8 | import sys 9 | import logging 10 | import argparse 11 | import json 12 | 13 | import yaml 14 | 15 | PROJ_ROOT = os.path.dirname(sys.argv[0]) 16 | OUTPUT_DIR = os.path.join(PROJ_ROOT, 'dashboards') 17 | CONFIG_FILE = os.path.join(PROJ_ROOT, 'config.yml') 18 | 19 | 20 | class ConfigObject(object): 21 | inherits = None 22 | 23 | def __init__(s, name, init_dict): 24 | s.__dict__.update(init_dict) 25 | s.name = name 26 | 27 | def __unicode__(s): 28 | return unicode(s.__dict__) 29 | 30 | def converge(s, others): 31 | """converge templates 32 | 33 | if ConfigObject has an inherits 'variable', fill in values from it. 34 | others == all ConfigObject's type instances from configfile (e.g. for 35 | converging dashboard templates, all dashboards found in config file 36 | 37 | convergence is recursive ("template might use "inherits" variable 38 | itself) 39 | 40 | inherited attribute is set to inherits, and inherits is set to None, to 41 | show, that template has already been instantiated and inherited 42 | attribute keeps the original template name for other purposes 43 | 44 | BEWARE OF LOOPS :-) 45 | """ 46 | if s.inherits is not None: 47 | data = others[s.inherits].converge(others) 48 | data.__dict__.update(s.__dict__) 49 | data.inherited = data.inherits 50 | data.inherits = None 51 | return data 52 | 53 | return s.__class__(s.name, s.__dict__) 54 | 55 | def fill(s, attr, others): 56 | """fill in attribute 57 | 58 | e.g. attr == 'rows', other == all rows in config file, 59 | items in s.rows attribute gets replaced with appropriate objects 60 | from others instantiating teplates on the way 61 | """ 62 | 63 | fill_list = [] 64 | for a in getattr(s, attr): 65 | fill_list.append(others[a].converge(others)) 66 | 67 | result = s.__class__(s.name, s.__dict__) 68 | setattr(result, attr, fill_list) 69 | 70 | return result 71 | 72 | def generate(s, context): 73 | result = {} 74 | 75 | for i in s.copy_items: 76 | result[i] = getattr(s, i) 77 | 78 | if hasattr(s, 'optional_copy_items'): 79 | for i in s.optional_copy_items: 80 | try: 81 | result[i] = getattr(s, i) 82 | except AttributeError: 83 | continue 84 | 85 | return result 86 | 87 | 88 | class Dashboard(ConfigObject): 89 | copy_items = ( 90 | 'editable', 91 | 'hideControls', 92 | 'originalTitle', 93 | 'refresh', 94 | 'sharedCrosshair', 95 | 'style', 96 | 'tags', 97 | 'timezone', 98 | 'title', 99 | ) 100 | optional_copy_items = ('folder',) 101 | 102 | instantiate = True 103 | title = 'Unnamed' 104 | originalTitle = 'Unnamed' 105 | 106 | tags = [] 107 | rows = [] 108 | dashboardLinks = [] 109 | templating = [] 110 | 111 | def generate(s, context): 112 | """TODO: I should check, that there are no "unused" items in the config 113 | (that would probably be a typo) 114 | """ 115 | 116 | result = super(Dashboard, s).generate(context) 117 | 118 | result['rows'] = [row.generate(context, s) for row in s.rows] 119 | 120 | result['links'] = [link.generate(context) for link in s.dashboardLinks] 121 | 122 | result['time'] = { 123 | 'from': s.time['from'], 124 | 'to': s.time['to'], 125 | } 126 | 127 | result['timepicker'] = { 128 | 'now': s.now, 129 | 'refresh_intervals': s.refresh_intervals, 130 | 'time_options': s.time_options, 131 | } 132 | 133 | result['templating'] = { 134 | 'list': [ 135 | template.generate(context, s) for template in s.templating], 136 | } 137 | 138 | result['annotations'] = { 139 | 'list': [], 140 | } 141 | 142 | result['schemaVersion'] = 7 143 | result['version'] = 22 144 | 145 | return result 146 | 147 | 148 | class Row(ConfigObject): 149 | """TODO: check type: matches with definition to avoid certain hard to find 150 | errors in config file 151 | 152 | also inheritance will inherit different types :-/ 153 | 154 | etc... 155 | 156 | FIXME: I presume row == panel 157 | """ 158 | 159 | _last_id = 0 160 | 161 | seriesOverrides = [] 162 | aliasColors = {} 163 | 164 | copy_items = ( 165 | 'collapse', 166 | 'editable', 167 | 'height', 168 | ) 169 | legend_items = ( 170 | 'legend_alignAsTable', 171 | 'legend_avg', 172 | 'legend_current', 173 | 'legend_max', 174 | 'legend_min', 175 | 'legend_show', 176 | 'legend_total', 177 | 'legend_values', 178 | ) 179 | grid_items = ( 180 | 'grid_leftLogBase', 181 | 'grid_leftMax', 182 | 'grid_leftMin', 183 | 'grid_rightLogBase', 184 | 'grid_rightMax', 185 | 'grid_rightMin', 186 | 'grid_threshold1', 187 | 'grid_threshold1Color', 188 | 'grid_threshold2', 189 | 'grid_threshold2Color', 190 | ) 191 | panel_copy_items = ( 192 | 'aliasColors', 193 | 'bars', 194 | 'datasource', 195 | 'editable', 196 | 'error', 197 | 'fill', 198 | 'leftYAxisLabel', 199 | 'lines', 200 | 'lineWidth', 201 | 'nullPointMode', 202 | 'percentage', 203 | 'pointradius', 204 | 'points', 205 | 'renderer', 206 | 'rightYAxisLabel', 207 | 'seriesOverrides', 208 | 'span', 209 | 'stack', 210 | 'steppedLine', 211 | 'timeFrom', 212 | 'title', 213 | 'type', 214 | ) 215 | 216 | @classmethod 217 | def gen_id(cls): 218 | cls._last_id += 1 219 | return cls._last_id 220 | 221 | def generate(s, context, parent_dashboard): 222 | result = super(Row, s).generate(context) 223 | 224 | result['title'] = 'Row' 225 | result['showTitle'] = False 226 | 227 | panel = {} 228 | 229 | for item in s.panel_copy_items: 230 | panel[item] = getattr(s, item) 231 | 232 | panel['id'] = Row.gen_id() 233 | panel['links'] = [] 234 | panel['timeShift'] = None 235 | panel['tooltip'] = { 236 | 'shared': s.tooltip_shared, 237 | 'value_type': 'cumulative'} 238 | panel['y_formats'] = s.y_formats 239 | 240 | panel['legend'] = {} 241 | lss = len('legend_') 242 | for li in s.legend_items: 243 | panel['legend'][li[lss:]] = getattr(s, li) 244 | 245 | panel['grid'] = {} 246 | gss = len('grid_') 247 | for gi in s.grid_items: 248 | panel['grid'][gi[gss:]] = getattr(s, gi) 249 | 250 | # targets - a.k.a. graphs/expressions in a single row 251 | assert len(s.targets) <= 26 # refId is a single letter (AFAIK) 252 | panel['targets'] = [] 253 | target_cnt = 0 254 | for target_data in s.targets: 255 | target = {} 256 | if 'legend_format' in target_data: 257 | target['legendFormat'] = target_data['legend_format'] 258 | else: 259 | target['legendFormat'] = s.legend_format 260 | if 'intervalFactor' in target_data: 261 | target['intervalFactor'] = target_data['intervalFactor'] 262 | else: 263 | target['intervalFactor'] = s.intervalFactor 264 | target['refId'] = chr(ord('A') + target_cnt) 265 | try: 266 | target['expr'] = target_data['expression'] % \ 267 | parent_dashboard.expvars 268 | except KeyError: 269 | print >>sys.stderr, "missing variable while trying to " \ 270 | "fill expr: %s" % target_data['expression'] 271 | print >>sys.stderr, "for dashboard %s" % parent_dashboard.title 272 | sys.exit(1) 273 | # workaround for double-\ in expressions 274 | target['expr'] = target['expr'].replace('\\', '\\\\') 275 | panel['targets'].append(target) 276 | target_cnt += 1 277 | 278 | result['panels'] = [panel] 279 | 280 | return result 281 | 282 | 283 | class Template(ConfigObject): 284 | copy_items = ( 285 | 'datasource', 286 | 'allValue', 287 | 'current', 288 | 'hide', 289 | 'includeAll', 290 | 'label', 291 | 'multi', 292 | 'options', 293 | 'refresh', 294 | 'sort', 295 | 'tagValuesQuery', 296 | 'tags', 297 | 'tagsQuery', 298 | 'type', 299 | 'useTags', 300 | ) 301 | 302 | def generate(s, context, parent_dashboard): 303 | result = super(Template, s).generate(context) 304 | 305 | # Set template name and query. 306 | result['name'] = s.name 307 | result['query'] = 'label_values(%s, %s)' % (s.metric, s.label) 308 | 309 | # This allows restricting values of the template by a regexp. 310 | # 311 | # When `templating_regexps` dict is defined on a dashboard and contains 312 | # a key with the same name as the name of the template, use a regexp 313 | # defined as an expvar with the same name as the value of the key. 314 | # This allows overriding the regexp per dashboard. 315 | if hasattr(parent_dashboard, 'templating_regexps') and \ 316 | s.name in parent_dashboard.templating_regexps: 317 | result['regex'] = parent_dashboard.expvars[ 318 | parent_dashboard.templating_regexps[s.name]] 319 | result['allValue'] = parent_dashboard.expvars[ 320 | parent_dashboard.templating_regexps 321 | [s.name]].replace('\\', '\\\\') 322 | 323 | return result 324 | 325 | 326 | class DashboardLink(ConfigObject): 327 | copy_items = ('icon', 'tags', 'targetBlank', 'type', 'url', 'title') 328 | 329 | def generate(s, context): 330 | return super(DashboardLink, s).generate(context) 331 | 332 | 333 | class YamlConfigParser(object): 334 | dashboards = {} 335 | graphs = {} 336 | rows = {} 337 | templating = {} 338 | dashboardLinks = {} 339 | 340 | def __init__(s, config_file='./config.yml'): 341 | s.config_file = config_file 342 | logging.debug("YamlConfigParser: reading %s" % s.config_file) 343 | with open(s.config_file, 'r') as f: 344 | s.yaml = yaml.load(f) 345 | 346 | def parse(s): 347 | logging.debug("YamlConfigParser: parsing") 348 | 349 | top_level_items = ( 350 | ('dashboards', Dashboard, s.dashboards), 351 | ('rows', Row, s.rows), 352 | ('templating', Template, s.templating), 353 | ('dashboardLinks', DashboardLink, s.dashboardLinks), 354 | ) 355 | 356 | for name, class_, store in top_level_items: 357 | for item in s.yaml[name]: 358 | store[item] = class_(item, s.yaml[name][item]) 359 | 360 | for dash in s.dashboards: 361 | # FIXME: not nice 362 | logging.debug("templatization & filling of %s dashboard" % dash) 363 | s.dashboards[dash] = s.dashboards[dash].converge(s.dashboards) 364 | # order of template instantiation and filling is not held (yaml 365 | # returns dicts even if they are ordered in config file), sometimes 366 | # we receive an already "filled" instance of dashboard when 367 | # inherited, check for that eventuality and skip fill() in that 368 | # case 369 | if len(s.dashboards[dash].rows) and \ 370 | not isinstance(s.dashboards[dash].rows[0], ConfigObject): 371 | s.dashboards[dash] = s.dashboards[dash].fill('rows', s.rows) 372 | if len(s.dashboards[dash].templating) and \ 373 | not isinstance(s.dashboards[dash].templating[0], Template): 374 | s.dashboards[dash] = s.dashboards[dash].fill( 375 | 'templating', s.templating) 376 | if len(s.dashboards[dash].dashboardLinks) and \ 377 | not isinstance( 378 | s.dashboards[dash].dashboardLinks[0], 379 | ConfigObject): 380 | s.dashboards[dash] = s.dashboards[dash].fill( 381 | 'dashboardLinks', s.dashboardLinks) 382 | 383 | 384 | class DashboardGenerator(object): 385 | def __init__(s, ycp): 386 | s.ycp = ycp 387 | 388 | def __iter__(s): 389 | for dash_name, dash in s.ycp.dashboards.iteritems(): 390 | if dash.instantiate: 391 | if hasattr(dash, 'folder'): 392 | folder = dash.folder 393 | delattr(dash, 'folder') 394 | else: 395 | folder = 'no_folder' # default folder 396 | yield dash_name, folder, s.gen_dashboard(dash) 397 | 398 | def gen_dashboard(s, d): 399 | return json.dumps(d.generate(s.ycp.dashboards)) 400 | 401 | 402 | def parse_args(): 403 | parser = argparse.ArgumentParser() 404 | parser.add_argument('-v', '--verbose', 405 | dest='verbose', action='store_true', 406 | help='be a lil bit verbose') 407 | parser.add_argument('-c', '--config-file', 408 | dest='config_file', 409 | default=CONFIG_FILE) 410 | parser.add_argument('-d', '--dest-dir', 411 | dest='dest_dir', 412 | default=OUTPUT_DIR) 413 | parser.add_argument('-n', '--noop', 414 | dest='noop', action='store_true', 415 | help="don't create json dashboard files") 416 | return parser.parse_args() 417 | 418 | 419 | def main(): 420 | logging.basicConfig(stream=sys.stderr) 421 | args = parse_args() 422 | 423 | if args.verbose: 424 | logging.getLogger().setLevel(logging.DEBUG) 425 | else: 426 | logging.getLogger().setLevel(logging.ERROR) 427 | 428 | ycp = YamlConfigParser(config_file=args.config_file) 429 | ycp.parse() 430 | dg = DashboardGenerator(ycp) 431 | 432 | if not args.noop: 433 | logging.debug('writing %s' % os.path.join(args.dest_dir, 'index')) 434 | index_f = open(os.path.join(args.dest_dir, 'index'), 'w') 435 | else: 436 | logging.debug('would be writing %s' % 437 | os.path.join(args.dest_dir, 'index')) 438 | 439 | for dashboard_name, dashboard_folder, dashboard in dg: 440 | out_fn = os.path.join(args.dest_dir, 441 | dashboard_folder, 442 | '%s.json' % dashboard_name) 443 | if args.noop: 444 | logging.debug('would be writing %s' % out_fn) 445 | else: 446 | logging.debug('writing %s' % out_fn) 447 | if dashboard_folder and not os.path.exists(os.path.dirname(out_fn)): 448 | os.makedirs(os.path.dirname(out_fn)) 449 | with open(out_fn, 'w') as f: 450 | print >>f, dashboard 451 | print >>index_f, os.path.join(dashboard_folder, 452 | '%s.json' % dashboard_name) 453 | if not args.noop: 454 | index_f.close() 455 | 456 | 457 | if __name__ == '__main__': 458 | main() 459 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyaml>=17.12.1 2 | PyYAML>=3.12 3 | --------------------------------------------------------------------------------