Ales Komarek | 3f044b2 | 2016-10-30 00:27:24 +0200 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | ''' |
| 3 | Manage Grafana v3.0 Dashboards |
| 4 | |
| 5 | .. versionadded:: 2016.3.0 |
| 6 | |
| 7 | .. code-block:: yaml |
| 8 | |
| 9 | grafana: |
| 10 | grafana_timeout: 3 |
| 11 | grafana_token: qwertyuiop |
| 12 | grafana_url: 'https://url.com' |
| 13 | |
| 14 | .. code-block:: yaml |
| 15 | |
| 16 | Ensure minimum dashboard is managed: |
| 17 | grafana_dashboard.present: |
| 18 | - name: insightful-dashboard |
| 19 | - base_dashboards_from_pillar: |
| 20 | - default_dashboard |
| 21 | - base_rows_from_pillar: |
| 22 | - default_row |
| 23 | - base_panels_from_pillar: |
| 24 | - default_panel |
| 25 | - dashboard: |
| 26 | rows: |
| 27 | - title: Usage |
| 28 | panels: |
| 29 | - targets: |
| 30 | - target: alias(constantLine(50), 'max') |
| 31 | title: Imaginary |
| 32 | type: graph |
| 33 | |
| 34 | |
| 35 | The behavior of this module is to create dashboards if they do not exist, to |
| 36 | add rows if they do not exist in existing dashboards, and to update rows if |
| 37 | they exist in dashboards. The module will not manage rows that are not defined, |
| 38 | allowing users to manage their own custom rows. |
| 39 | ''' |
| 40 | |
| 41 | # Import Python libs |
| 42 | from __future__ import absolute_import |
| 43 | import copy |
| 44 | import json |
| 45 | import requests |
| 46 | |
| 47 | # Import Salt libs |
| 48 | import salt.ext.six as six |
| 49 | from salt.utils.dictdiffer import DictDiffer |
| 50 | |
| 51 | |
| 52 | def __virtual__(): |
| 53 | '''Only load if grafana v2.0 is configured.''' |
| 54 | return __salt__['config.get']('grafana_version', 1) == 3 |
| 55 | |
| 56 | |
| 57 | _DEFAULT_DASHBOARD_PILLAR = 'grafana_dashboards:default' |
| 58 | _DEFAULT_PANEL_PILLAR = 'grafana_panels:default' |
| 59 | _DEFAULT_ROW_PILLAR = 'grafana_rows:default' |
| 60 | _PINNED_ROWS_PILLAR = 'grafana_pinned_rows' |
| 61 | |
| 62 | |
| 63 | def present(name, |
| 64 | base_dashboards_from_pillar=None, |
| 65 | base_panels_from_pillar=None, |
| 66 | base_rows_from_pillar=None, |
| 67 | dashboard=None, |
Guillaume Thouvenin | 2958bda | 2016-11-08 11:55:55 +0100 | [diff] [blame] | 68 | dashboard_format='yaml', |
Ales Komarek | 3f044b2 | 2016-10-30 00:27:24 +0200 | [diff] [blame] | 69 | profile='grafana'): |
| 70 | ''' |
| 71 | Ensure the grafana dashboard exists and is managed. |
| 72 | |
| 73 | name |
| 74 | Name of the grafana dashboard. |
| 75 | |
| 76 | base_dashboards_from_pillar |
| 77 | A pillar key that contains a list of dashboards to inherit from |
| 78 | |
| 79 | base_panels_from_pillar |
| 80 | A pillar key that contains a list of panels to inherit from |
| 81 | |
| 82 | base_rows_from_pillar |
| 83 | A pillar key that contains a list of rows to inherit from |
| 84 | |
| 85 | dashboard |
| 86 | A dict that defines a dashboard that should be managed. |
| 87 | |
Guillaume Thouvenin | 2958bda | 2016-11-08 11:55:55 +0100 | [diff] [blame] | 88 | dashboard_format |
| 89 | You can use two formats for dashboards. You can use the JSON format |
| 90 | if you provide a complete dashboard in raw JSON or you can use the YAML |
| 91 | format (this is the default) and provide a description of the |
| 92 | dashboard in YAML. |
| 93 | |
Ales Komarek | 3f044b2 | 2016-10-30 00:27:24 +0200 | [diff] [blame] | 94 | profile |
| 95 | A pillar key or dict that contains grafana information |
| 96 | ''' |
| 97 | ret = {'name': name, 'result': True, 'comment': '', 'changes': {}} |
Ales Komarek | 3f044b2 | 2016-10-30 00:27:24 +0200 | [diff] [blame] | 98 | dashboard = dashboard or {} |
| 99 | |
| 100 | if isinstance(profile, six.string_types): |
| 101 | profile = __salt__['config.option'](profile) |
| 102 | |
Guillaume Thouvenin | 2958bda | 2016-11-08 11:55:55 +0100 | [diff] [blame] | 103 | if dashboard_format == 'json': |
| 104 | # In this case, a raw JSON of the full dashboard is provided. |
| 105 | response = _update(dashboard, profile) |
| 106 | |
| 107 | if response.get('status') == 'success': |
| 108 | ret['comment'] = 'Dashboard {0} created.'.format(name) |
| 109 | ret['changes']['new'] = 'Dashboard {0} created.'.format(name) |
| 110 | else: |
| 111 | ret['result'] = False |
| 112 | ret['comment'] = ("Failed to create dashboard {0}, " |
| 113 | "response={1}").format(name, response) |
| 114 | |
| 115 | return ret |
| 116 | |
| 117 | base_dashboards_from_pillar = base_dashboards_from_pillar or [] |
| 118 | base_panels_from_pillar = base_panels_from_pillar or [] |
| 119 | base_rows_from_pillar = base_rows_from_pillar or [] |
| 120 | |
Ales Komarek | 3f044b2 | 2016-10-30 00:27:24 +0200 | [diff] [blame] | 121 | # Add pillar keys for default configuration |
| 122 | base_dashboards_from_pillar = ([_DEFAULT_DASHBOARD_PILLAR] + |
| 123 | base_dashboards_from_pillar) |
| 124 | base_panels_from_pillar = ([_DEFAULT_PANEL_PILLAR] + |
| 125 | base_panels_from_pillar) |
| 126 | base_rows_from_pillar = [_DEFAULT_ROW_PILLAR] + base_rows_from_pillar |
| 127 | |
| 128 | # Build out all dashboard fields |
| 129 | new_dashboard = _inherited_dashboard( |
| 130 | dashboard, base_dashboards_from_pillar, ret) |
Guillaume Thouvenin | 885c5fc | 2017-01-10 17:08:26 +0100 | [diff] [blame] | 131 | if not new_dashboard.get('title'): |
| 132 | new_dashboard['title'] = name |
Ales Komarek | 3f044b2 | 2016-10-30 00:27:24 +0200 | [diff] [blame] | 133 | rows = new_dashboard.get('rows', []) |
| 134 | for i, row in enumerate(rows): |
| 135 | rows[i] = _inherited_row(row, base_rows_from_pillar, ret) |
| 136 | for row in rows: |
| 137 | panels = row.get('panels', []) |
Guillaume Thouvenin | ab1a01b | 2017-01-25 16:01:40 +0100 | [diff] [blame] | 138 | for i, panel in enumerate(sorted(panels)): |
Ales Komarek | 3f044b2 | 2016-10-30 00:27:24 +0200 | [diff] [blame] | 139 | panels[i] = _inherited_panel(panel, base_panels_from_pillar, ret) |
| 140 | _auto_adjust_panel_spans(new_dashboard) |
| 141 | _ensure_panel_ids(new_dashboard) |
| 142 | _ensure_annotations(new_dashboard) |
| 143 | |
| 144 | # Create dashboard if it does not exist |
| 145 | url = 'db/{0}'.format(name) |
| 146 | old_dashboard = _get(url, profile) |
| 147 | if not old_dashboard: |
| 148 | if __opts__['test']: |
| 149 | ret['result'] = None |
| 150 | ret['comment'] = 'Dashboard {0} is set to be created.'.format(name) |
| 151 | return ret |
| 152 | |
| 153 | response = _update(new_dashboard, profile) |
| 154 | if response.get('status') == 'success': |
| 155 | ret['comment'] = 'Dashboard {0} created.'.format(name) |
| 156 | ret['changes']['new'] = 'Dashboard {0} created.'.format(name) |
| 157 | else: |
| 158 | ret['result'] = False |
| 159 | ret['comment'] = ("Failed to create dashboard {0}, " |
| 160 | "response={1}").format(name, response) |
| 161 | return ret |
| 162 | |
| 163 | # Add unmanaged rows to the dashboard. They appear at the top if they are |
| 164 | # marked as pinned. They appear at the bottom otherwise. |
| 165 | managed_row_titles = [row.get('title') |
| 166 | for row in new_dashboard.get('rows', [])] |
| 167 | new_rows = new_dashboard.get('rows', []) |
| 168 | for old_row in old_dashboard.get('rows', []): |
| 169 | if old_row.get('title') not in managed_row_titles: |
| 170 | new_rows.append(copy.deepcopy(old_row)) |
| 171 | _ensure_pinned_rows(new_dashboard) |
| 172 | _ensure_panel_ids(new_dashboard) |
| 173 | |
| 174 | # Update dashboard if it differs |
| 175 | dashboard_diff = DictDiffer(_cleaned(new_dashboard), |
| 176 | _cleaned(old_dashboard)) |
| 177 | updated_needed = (dashboard_diff.changed() or |
| 178 | dashboard_diff.added() or |
| 179 | dashboard_diff.removed()) |
| 180 | if updated_needed: |
| 181 | if __opts__['test']: |
| 182 | ret['result'] = None |
| 183 | ret['comment'] = ('Dashboard {0} is set to be updated, ' |
| 184 | 'changes={1}').format( |
| 185 | name, |
| 186 | json.dumps( |
| 187 | _dashboard_diff( |
| 188 | _cleaned(new_dashboard), |
| 189 | _cleaned(old_dashboard) |
| 190 | ), |
| 191 | indent=4 |
| 192 | )) |
| 193 | return ret |
| 194 | |
| 195 | response = _update(new_dashboard, profile) |
| 196 | if response.get('status') == 'success': |
| 197 | updated_dashboard = _get(url, profile) |
| 198 | dashboard_diff = DictDiffer(_cleaned(updated_dashboard), |
| 199 | _cleaned(old_dashboard)) |
| 200 | ret['comment'] = 'Dashboard {0} updated.'.format(name) |
| 201 | ret['changes'] = _dashboard_diff(_cleaned(new_dashboard), |
| 202 | _cleaned(old_dashboard)) |
| 203 | else: |
| 204 | ret['result'] = False |
| 205 | ret['comment'] = ("Failed to update dashboard {0}, " |
| 206 | "response={1}").format(name, response) |
| 207 | return ret |
| 208 | |
| 209 | ret['comment'] = 'Dashboard present' |
| 210 | return ret |
| 211 | |
| 212 | |
| 213 | def absent(name, profile='grafana'): |
| 214 | ''' |
| 215 | Ensure the named grafana dashboard is absent. |
| 216 | |
| 217 | name |
| 218 | Name of the grafana dashboard. |
| 219 | |
| 220 | profile |
| 221 | A pillar key or dict that contains grafana information |
| 222 | ''' |
| 223 | ret = {'name': name, 'result': True, 'comment': '', 'changes': {}} |
| 224 | |
| 225 | if isinstance(profile, six.string_types): |
| 226 | profile = __salt__['config.option'](profile) |
| 227 | |
| 228 | url = 'db/{0}'.format(name) |
| 229 | existing_dashboard = _get(url, profile) |
| 230 | if existing_dashboard: |
| 231 | if __opts__['test']: |
| 232 | ret['result'] = None |
| 233 | ret['comment'] = 'Dashboard {0} is set to be deleted.'.format(name) |
| 234 | return ret |
| 235 | |
| 236 | _delete(url, profile) |
| 237 | ret['comment'] = 'Dashboard {0} deleted.'.format(name) |
| 238 | ret['changes']['new'] = 'Dashboard {0} deleted.'.format(name) |
| 239 | return ret |
| 240 | |
| 241 | ret['comment'] = 'Dashboard absent' |
| 242 | return ret |
| 243 | |
| 244 | |
| 245 | _IGNORED_DASHBOARD_FIELDS = [ |
| 246 | 'id', |
| 247 | 'originalTitle', |
| 248 | 'version', |
| 249 | ] |
| 250 | _IGNORED_ROW_FIELDS = [] |
| 251 | _IGNORED_PANEL_FIELDS = [ |
| 252 | 'grid', |
| 253 | 'mode', |
| 254 | 'tooltip', |
| 255 | ] |
| 256 | _IGNORED_TARGET_FIELDS = [ |
| 257 | 'textEditor', |
| 258 | ] |
| 259 | |
| 260 | |
| 261 | def _cleaned(_dashboard): |
| 262 | '''Return a copy without fields that can differ.''' |
| 263 | dashboard = copy.deepcopy(_dashboard) |
| 264 | |
| 265 | for ignored_dashboard_field in _IGNORED_DASHBOARD_FIELDS: |
| 266 | dashboard.pop(ignored_dashboard_field, None) |
| 267 | for row in dashboard.get('rows', []): |
| 268 | for ignored_row_field in _IGNORED_ROW_FIELDS: |
| 269 | row.pop(ignored_row_field, None) |
| 270 | for i, panel in enumerate(row.get('panels', [])): |
| 271 | for ignored_panel_field in _IGNORED_PANEL_FIELDS: |
| 272 | panel.pop(ignored_panel_field, None) |
| 273 | for target in panel.get('targets', []): |
| 274 | for ignored_target_field in _IGNORED_TARGET_FIELDS: |
| 275 | target.pop(ignored_target_field, None) |
| 276 | row['panels'][i] = _stripped(panel) |
| 277 | |
| 278 | return dashboard |
| 279 | |
| 280 | |
| 281 | def _inherited_dashboard(dashboard, base_dashboards_from_pillar, ret): |
| 282 | '''Return a dashboard with properties from parents.''' |
| 283 | base_dashboards = [] |
| 284 | for base_dashboard_from_pillar in base_dashboards_from_pillar: |
| 285 | base_dashboard = __salt__['pillar.get'](base_dashboard_from_pillar) |
| 286 | if base_dashboard: |
| 287 | base_dashboards.append(base_dashboard) |
| 288 | elif base_dashboard_from_pillar != _DEFAULT_DASHBOARD_PILLAR: |
| 289 | ret.setdefault('warnings', []) |
| 290 | warning_message = 'Cannot find dashboard pillar "{0}".'.format( |
| 291 | base_dashboard_from_pillar) |
| 292 | if warning_message not in ret['warnings']: |
| 293 | ret['warnings'].append(warning_message) |
| 294 | base_dashboards.append(dashboard) |
| 295 | |
| 296 | result_dashboard = {} |
| 297 | tags = set() |
| 298 | for dashboard in base_dashboards: |
| 299 | tags.update(dashboard.get('tags', [])) |
| 300 | result_dashboard.update(dashboard) |
| 301 | result_dashboard['tags'] = list(tags) |
| 302 | return result_dashboard |
| 303 | |
| 304 | |
| 305 | def _inherited_row(row, base_rows_from_pillar, ret): |
| 306 | '''Return a row with properties from parents.''' |
| 307 | base_rows = [] |
| 308 | for base_row_from_pillar in base_rows_from_pillar: |
| 309 | base_row = __salt__['pillar.get'](base_row_from_pillar) |
| 310 | if base_row: |
| 311 | base_rows.append(base_row) |
| 312 | elif base_row_from_pillar != _DEFAULT_ROW_PILLAR: |
| 313 | ret.setdefault('warnings', []) |
| 314 | warning_message = 'Cannot find row pillar "{0}".'.format( |
| 315 | base_row_from_pillar) |
| 316 | if warning_message not in ret['warnings']: |
| 317 | ret['warnings'].append(warning_message) |
| 318 | base_rows.append(row) |
| 319 | |
| 320 | result_row = {} |
| 321 | for row in base_rows: |
| 322 | result_row.update(row) |
| 323 | return result_row |
| 324 | |
| 325 | |
| 326 | def _inherited_panel(panel, base_panels_from_pillar, ret): |
| 327 | '''Return a panel with properties from parents.''' |
| 328 | base_panels = [] |
| 329 | for base_panel_from_pillar in base_panels_from_pillar: |
| 330 | base_panel = __salt__['pillar.get'](base_panel_from_pillar) |
| 331 | if base_panel: |
| 332 | base_panels.append(base_panel) |
| 333 | elif base_panel_from_pillar != _DEFAULT_PANEL_PILLAR: |
| 334 | ret.setdefault('warnings', []) |
| 335 | warning_message = 'Cannot find panel pillar "{0}".'.format( |
| 336 | base_panel_from_pillar) |
| 337 | if warning_message not in ret['warnings']: |
| 338 | ret['warnings'].append(warning_message) |
| 339 | base_panels.append(panel) |
| 340 | |
| 341 | result_panel = {} |
| 342 | for panel in base_panels: |
| 343 | result_panel.update(panel) |
| 344 | return result_panel |
| 345 | |
| 346 | |
| 347 | _FULL_LEVEL_SPAN = 12 |
| 348 | _DEFAULT_PANEL_SPAN = 2.5 |
| 349 | |
| 350 | |
| 351 | def _auto_adjust_panel_spans(dashboard): |
| 352 | '''Adjust panel spans to take up the available width. |
| 353 | |
| 354 | For each group of panels that would be laid out on the same level, scale up |
| 355 | the unspecified panel spans to fill up the level. |
| 356 | ''' |
| 357 | for row in dashboard.get('rows', []): |
| 358 | levels = [] |
| 359 | current_level = [] |
| 360 | levels.append(current_level) |
| 361 | for panel in row.get('panels', []): |
| 362 | current_level_span = sum(panel.get('span', _DEFAULT_PANEL_SPAN) |
| 363 | for panel in current_level) |
| 364 | span = panel.get('span', _DEFAULT_PANEL_SPAN) |
| 365 | if current_level_span + span > _FULL_LEVEL_SPAN: |
| 366 | current_level = [panel] |
| 367 | levels.append(current_level) |
| 368 | else: |
| 369 | current_level.append(panel) |
| 370 | |
| 371 | for level in levels: |
| 372 | specified_panels = [panel for panel in level if 'span' in panel] |
| 373 | unspecified_panels = [panel for panel in level |
| 374 | if 'span' not in panel] |
| 375 | if not unspecified_panels: |
| 376 | continue |
| 377 | |
| 378 | specified_span = sum(panel['span'] for panel in specified_panels) |
| 379 | available_span = _FULL_LEVEL_SPAN - specified_span |
| 380 | auto_span = float(available_span) / len(unspecified_panels) |
| 381 | for panel in unspecified_panels: |
| 382 | panel['span'] = auto_span |
| 383 | |
| 384 | |
| 385 | def _ensure_pinned_rows(dashboard): |
| 386 | '''Pin rows to the top of the dashboard.''' |
| 387 | pinned_row_titles = __salt__['pillar.get'](_PINNED_ROWS_PILLAR) |
| 388 | if not pinned_row_titles: |
| 389 | return |
| 390 | |
| 391 | pinned_row_titles_lower = [] |
| 392 | for title in pinned_row_titles: |
| 393 | pinned_row_titles_lower.append(title.lower()) |
| 394 | rows = dashboard.get('rows', []) |
| 395 | pinned_rows = [] |
| 396 | for i, row in enumerate(rows): |
| 397 | if row.get('title', '').lower() in pinned_row_titles_lower: |
| 398 | del rows[i] |
| 399 | pinned_rows.append(row) |
| 400 | rows = pinned_rows + rows |
| 401 | |
| 402 | |
| 403 | def _ensure_panel_ids(dashboard): |
| 404 | '''Assign panels auto-incrementing IDs.''' |
| 405 | panel_id = 1 |
| 406 | for row in dashboard.get('rows', []): |
| 407 | for panel in row.get('panels', []): |
| 408 | panel['id'] = panel_id |
| 409 | panel_id += 1 |
| 410 | |
| 411 | |
| 412 | def _ensure_annotations(dashboard): |
| 413 | '''Explode annotation_tags into annotations.''' |
| 414 | if 'annotation_tags' not in dashboard: |
| 415 | return |
| 416 | tags = dashboard['annotation_tags'] |
| 417 | annotations = { |
| 418 | 'enable': True, |
| 419 | 'list': [], |
| 420 | } |
| 421 | for tag in tags: |
| 422 | annotations['list'].append({ |
| 423 | 'datasource': "graphite", |
| 424 | 'enable': False, |
| 425 | 'iconColor': "#C0C6BE", |
| 426 | 'iconSize': 13, |
| 427 | 'lineColor': "rgba(255, 96, 96, 0.592157)", |
| 428 | 'name': tag, |
| 429 | 'showLine': True, |
| 430 | 'tags': tag, |
| 431 | }) |
| 432 | del dashboard['annotation_tags'] |
| 433 | dashboard['annotations'] = annotations |
| 434 | |
| 435 | |
| 436 | def _get(url, profile): |
| 437 | '''Get a specific dashboard.''' |
| 438 | request_url = "{0}/api/dashboards/{1}".format(profile.get('grafana_url'), |
| 439 | url) |
| 440 | if profile.get('grafana_token', False): |
| 441 | response = requests.get( |
| 442 | request_url, |
| 443 | headers=_get_headers(profile), |
| 444 | timeout=profile.get('grafana_timeout', 3), |
| 445 | ) |
| 446 | else: |
| 447 | response = requests.get( |
| 448 | request_url, |
| 449 | auth=_get_auth(profile), |
| 450 | timeout=profile.get('grafana_timeout', 3), |
| 451 | ) |
| 452 | data = response.json() |
| 453 | if data.get('message') == 'Not found': |
| 454 | return None |
| 455 | if 'dashboard' not in data: |
| 456 | return None |
| 457 | return data['dashboard'] |
| 458 | |
| 459 | |
| 460 | def _delete(url, profile): |
| 461 | '''Delete a specific dashboard.''' |
| 462 | request_url = "{0}/api/dashboards/{1}".format(profile.get('grafana_url'), |
| 463 | url) |
Guillaume Thouvenin | 2958bda | 2016-11-08 11:55:55 +0100 | [diff] [blame] | 464 | response = requests.delete( |
| 465 | request_url, |
| 466 | auth=_get_auth(profile), |
| 467 | headers=_get_headers(profile), |
| 468 | timeout=profile.get('grafana_timeout'), |
| 469 | ) |
Ales Komarek | 3f044b2 | 2016-10-30 00:27:24 +0200 | [diff] [blame] | 470 | data = response.json() |
| 471 | return data |
| 472 | |
| 473 | |
| 474 | def _update(dashboard, profile): |
| 475 | '''Update a specific dashboard.''' |
| 476 | payload = { |
| 477 | 'dashboard': dashboard, |
| 478 | 'overwrite': True |
| 479 | } |
Guillaume Thouvenin | 2958bda | 2016-11-08 11:55:55 +0100 | [diff] [blame] | 480 | response = requests.post( |
| 481 | "{0}/api/dashboards/db".format(profile.get('grafana_url')), |
| 482 | auth=_get_auth(profile), |
| 483 | headers=_get_headers(profile), |
| 484 | json=payload |
| 485 | ) |
Ales Komarek | 3f044b2 | 2016-10-30 00:27:24 +0200 | [diff] [blame] | 486 | return response.json() |
| 487 | |
| 488 | |
| 489 | def _get_headers(profile): |
Guillaume Thouvenin | 2958bda | 2016-11-08 11:55:55 +0100 | [diff] [blame] | 490 | headers = {'Content-type': 'application/json'} |
| 491 | |
| 492 | if profile.get('grafana_token', False): |
| 493 | headers['Authorization'] = 'Bearer {0}'.format(profile['grafana_token']) |
| 494 | |
| 495 | return headers |
Ales Komarek | 3f044b2 | 2016-10-30 00:27:24 +0200 | [diff] [blame] | 496 | |
| 497 | |
| 498 | def _get_auth(profile): |
Guillaume Thouvenin | 2958bda | 2016-11-08 11:55:55 +0100 | [diff] [blame] | 499 | if profile.get('grafana_token', False): |
| 500 | return None |
| 501 | |
Ales Komarek | 3f044b2 | 2016-10-30 00:27:24 +0200 | [diff] [blame] | 502 | return requests.auth.HTTPBasicAuth( |
| 503 | profile['grafana_user'], |
| 504 | profile['grafana_password'] |
| 505 | ) |
| 506 | |
| 507 | |
| 508 | def _dashboard_diff(_new_dashboard, _old_dashboard): |
| 509 | '''Return a dictionary of changes between dashboards.''' |
| 510 | diff = {} |
| 511 | |
| 512 | # Dashboard diff |
| 513 | new_dashboard = copy.deepcopy(_new_dashboard) |
| 514 | old_dashboard = copy.deepcopy(_old_dashboard) |
| 515 | dashboard_diff = DictDiffer(new_dashboard, old_dashboard) |
| 516 | diff['dashboard'] = _stripped({ |
| 517 | 'changed': list(dashboard_diff.changed()) or None, |
| 518 | 'added': list(dashboard_diff.added()) or None, |
| 519 | 'removed': list(dashboard_diff.removed()) or None, |
| 520 | }) |
| 521 | |
| 522 | # Row diff |
| 523 | new_rows = new_dashboard.get('rows', []) |
| 524 | old_rows = old_dashboard.get('rows', []) |
| 525 | new_rows_by_title = {} |
| 526 | old_rows_by_title = {} |
| 527 | for row in new_rows: |
| 528 | if 'title' in row: |
| 529 | new_rows_by_title[row['title']] = row |
| 530 | for row in old_rows: |
| 531 | if 'title' in row: |
| 532 | old_rows_by_title[row['title']] = row |
| 533 | rows_diff = DictDiffer(new_rows_by_title, old_rows_by_title) |
| 534 | diff['rows'] = _stripped({ |
| 535 | 'added': list(rows_diff.added()) or None, |
| 536 | 'removed': list(rows_diff.removed()) or None, |
| 537 | }) |
| 538 | for changed_row_title in rows_diff.changed(): |
| 539 | old_row = old_rows_by_title[changed_row_title] |
| 540 | new_row = new_rows_by_title[changed_row_title] |
| 541 | row_diff = DictDiffer(new_row, old_row) |
| 542 | diff['rows'].setdefault('changed', {}) |
| 543 | diff['rows']['changed'][changed_row_title] = _stripped({ |
| 544 | 'changed': list(row_diff.changed()) or None, |
| 545 | 'added': list(row_diff.added()) or None, |
| 546 | 'removed': list(row_diff.removed()) or None, |
| 547 | }) |
| 548 | |
| 549 | # Panel diff |
| 550 | old_panels_by_id = {} |
| 551 | new_panels_by_id = {} |
| 552 | for row in old_dashboard.get('rows', []): |
| 553 | for panel in row.get('panels', []): |
| 554 | if 'id' in panel: |
| 555 | old_panels_by_id[panel['id']] = panel |
| 556 | for row in new_dashboard.get('rows', []): |
| 557 | for panel in row.get('panels', []): |
| 558 | if 'id' in panel: |
| 559 | new_panels_by_id[panel['id']] = panel |
| 560 | panels_diff = DictDiffer(new_panels_by_id, old_panels_by_id) |
| 561 | diff['panels'] = _stripped({ |
| 562 | 'added': list(panels_diff.added()) or None, |
| 563 | 'removed': list(panels_diff.removed()) or None, |
| 564 | }) |
| 565 | for changed_panel_id in panels_diff.changed(): |
| 566 | old_panel = old_panels_by_id[changed_panel_id] |
| 567 | new_panel = new_panels_by_id[changed_panel_id] |
| 568 | panels_diff = DictDiffer(new_panel, old_panel) |
| 569 | diff['panels'].setdefault('changed', {}) |
| 570 | diff['panels']['changed'][changed_panel_id] = _stripped({ |
| 571 | 'changed': list(panels_diff.changed()) or None, |
| 572 | 'added': list(panels_diff.added()) or None, |
| 573 | 'removed': list(panels_diff.removed()) or None, |
| 574 | }) |
| 575 | |
| 576 | return diff |
| 577 | |
| 578 | |
| 579 | def _stripped(d): |
| 580 | '''Strip falsey entries.''' |
| 581 | ret = {} |
| 582 | for k, v in six.iteritems(d): |
| 583 | if v: |
| 584 | ret[k] = v |
Guillaume Thouvenin | 2958bda | 2016-11-08 11:55:55 +0100 | [diff] [blame] | 585 | return ret |