Skip to content
Snippets Groups Projects
Commit f7afabe2 authored by Bjarke Madsen's avatar Bjarke Madsen
Browse files

Remove use of multiprocessing and simplify codebase a bit

Obtain significant speedups by looking up folder dashboards in advance
parent 591768bd
No related branches found
No related tags found
No related merge requests found
...@@ -6,6 +6,7 @@ import os ...@@ -6,6 +6,7 @@ import os
import json import json
import time import time
from typing import Dict, Any, Optional
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from brian_dashboard_manager.grafana.utils.request import TokenRequest from brian_dashboard_manager.grafana.utils.request import TokenRequest
...@@ -191,7 +192,8 @@ def _get_dashboard(request: TokenRequest, uid): ...@@ -191,7 +192,8 @@ def _get_dashboard(request: TokenRequest, uid):
return r.json()['dashboard'] return r.json()['dashboard']
def create_dashboard(request: TokenRequest, dashboard: dict, folder_id=None): def create_dashboard(request: TokenRequest, dashboard: dict, folder_id=None,
existing_folder_dashboards: Optional[Dict[str, Any]] = None):
""" """
Creates the given dashboard for the organization tied to the token. Creates the given dashboard for the organization tied to the token.
If the dashboard already exists, it will be updated. If the dashboard already exists, it will be updated.
...@@ -205,22 +207,32 @@ def create_dashboard(request: TokenRequest, dashboard: dict, folder_id=None): ...@@ -205,22 +207,32 @@ def create_dashboard(request: TokenRequest, dashboard: dict, folder_id=None):
title = dashboard['title'] title = dashboard['title']
existing_dashboard = None existing_dashboard = None
has_uid = dashboard.get('uid') is not None has_uid = dashboard.get('uid') is not None
if has_uid:
existing_dashboard = _get_dashboard(request, uid=dashboard['uid']) existing_dashboard = None
else:
existing_dashboard = _search_dashboard(request, dashboard, folder_id) if existing_folder_dashboards:
existing_dashboard = existing_folder_dashboards.get(dashboard['title'].lower())
if has_uid:
assert existing_dashboard is None or existing_dashboard['uid'] == dashboard['uid'], \
f"UID mismatch for dashboard {title}: {existing_dashboard['uid']} != {dashboard['uid']}"
if not existing_dashboard:
if has_uid:
existing_dashboard = _get_dashboard(request, uid=dashboard['uid'])
else:
existing_dashboard = _search_dashboard(request, dashboard, folder_id)
if existing_dashboard: if existing_dashboard:
dashboard['uid'] = existing_dashboard['uid'] dashboard['uid'] = existing_dashboard['uid']
dashboard['id'] = existing_dashboard['id'] dashboard['id'] = existing_dashboard['id']
dashboard['version'] = existing_dashboard['version'] dashboard['version'] = 1
else: else:
# We are creating a new dashboard, delete ID if it exists. # We are creating a new dashboard, delete ID if it exists.
dashboard.pop('id', None) dashboard.pop('id', None)
payload = { payload = {
'dashboard': dashboard, 'dashboard': dashboard,
'overwrite': False 'overwrite': True
} }
if folder_id: if folder_id:
payload['folderId'] = folder_id payload['folderId'] = folder_id
......
...@@ -6,6 +6,24 @@ from brian_dashboard_manager.grafana.utils.request import TokenRequest ...@@ -6,6 +6,24 @@ from brian_dashboard_manager.grafana.utils.request import TokenRequest
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def list_folder_dashboards(request: TokenRequest, folder_uid):
"""
Lists all dashboards in a folder.
:param request: TokenRequest object
:param folder_uid: folder UID
:return: list of dashboard definitions
"""
try:
r = request.get(f'api/search?folderUIDs={folder_uid}')
dashboards = r.json()
except HTTPError:
logger.exception(f'Error when listing dashboards in folder: {folder_uid}')
return []
return {dash['title'].lower(): dash for dash in dashboards}
def delete_folder(request: TokenRequest, title=None, uid=None): def delete_folder(request: TokenRequest, title=None, uid=None):
""" """
Deletes a single folder for the organization Deletes a single folder for the organization
......
This diff is collapsed.
...@@ -2,9 +2,11 @@ ...@@ -2,9 +2,11 @@
Helper functions used to group interfaces together and generate the Helper functions used to group interfaces together and generate the
necessary data to generate the dashboards from templates. necessary data to generate the dashboards from templates.
""" """
from collections import defaultdict
from concurrent.futures import ProcessPoolExecutor
import logging import logging
from typing import Union
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
from itertools import product from itertools import product
from functools import partial, reduce from functools import partial, reduce
from string import ascii_uppercase from string import ascii_uppercase
...@@ -12,7 +14,9 @@ from string import ascii_uppercase ...@@ -12,7 +14,9 @@ from string import ascii_uppercase
from brian_dashboard_manager.templating.render import create_panel, \ from brian_dashboard_manager.templating.render import create_panel, \
create_panel_target, create_dropdown_panel create_panel_target, create_dropdown_panel
NUM_PROCESSES = 4 AnyExecutor = Union[ProcessPoolExecutor, ThreadPoolExecutor]
DEFAULT_TIMEOUT = 60
PANEL_HEIGHT = 12 PANEL_HEIGHT = 12
PANEL_WIDTH = 24 PANEL_WIDTH = 24
...@@ -686,29 +690,122 @@ def default_interface_panel_generator(gridPos, use_all_traffic=True, use_ipv6=Tr ...@@ -686,29 +690,122 @@ def default_interface_panel_generator(gridPos, use_all_traffic=True, use_ipv6=Tr
return get_panel_definitions return get_panel_definitions
def _get_dashboard_data(data, datasource, tag, single_data_func): def create_aggregate_panel(title, gridpos, targets, datasource):
""" """
Helper for generating dashboard definitions. Helper for generating aggregate panels. Creates two panels, one for
Uses multiprocessing to speed up generation. ingress and one for egress.
:param data: the dashboard names and the panel data for each dashboard :param title: title for the panel
:param gridpos: generator for grid position
:param targets: list of targets for the panels, used to build separate
targets for both ingress and egress.
:param datasource: datasource to use for the panels
:return: tuple of aggregate panels, one for ingress and one for egress
"""
ingress_targets, egress_targets = get_aggregate_targets(targets)
ingress_pos = next(gridpos)
egress_pos = next(gridpos)
is_total = 'totals' in title.lower()
def reduce_alias(prev, curr):
alias = curr['alias']
if 'egress' in alias.lower():
prev[alias] = '#0000FF'
else:
prev[alias] = '#00FF00'
return prev
ingress_colors = reduce(reduce_alias, ingress_targets, {})
egress_colors = reduce(reduce_alias, egress_targets, {})
ingress = create_panel(
**ingress_pos,
stack=True,
linewidth=0 if is_total else 1,
datasource=datasource,
title=title + " - ingress",
targets=ingress_targets,
y_axis_type="bits",
alias_colors=ingress_colors if is_total else {},
)
egress = create_panel(
**egress_pos,
stack=True,
linewidth=0 if is_total else 1,
datasource=datasource,
title=title + " - egress",
targets=egress_targets,
y_axis_type="bits",
alias_colors=egress_colors if is_total else {},
)
return ingress, egress
def get_aggregate_dashboard_data(title, remotes, datasource, tag):
"""
Helper for generating aggregate dashboard definitions.
Aggregate dashboards consist only of aggregate panels that are
panels with data for multiple interfaces.
At the top of the dashboard are two aggregate panels showing
total ingress and egress data for all interfaces.
Below that are two aggregate panels for each target, one for
ingress and one for egress.
:param title: title for the dashboard
:param remotes: dictionary of targets for the panels, the key is the
remote (usually a customer) and the value is a list of targets
for that remote. A single target represents how to fetch
data for one interface.
:param datasource: datasource to use for the panels :param datasource: datasource to use for the panels
:param tag: tag to use for the dashboard, used for dashboard dropdowns on :param tag: tag to use for the dashboard, used for dashboard dropdowns on
the home dashboard. the home dashboard.
:param single_data_func: function that gets data for one definition
:return: generator for dashboard definitions for each dashboard :return: dashboard definition for the aggregate dashboard
""" """
with ProcessPoolExecutor(max_workers=NUM_PROCESSES) as executor: id_gen = num_generator()
for dash in executor.map( gridPos = gridPos_generator(id_gen, agg=True)
partial(
single_data_func, panels = []
datasource=datasource, all_targets = remotes.get('EVERYSINGLETARGET', [])
tag=tag),
data.items() ingress, egress = create_aggregate_panel(
): title, gridPos, all_targets, datasource)
yield dash panels.extend([ingress, egress])
totals_title = title + ' - Totals'
t_in, t_eg = create_aggregate_panel(
totals_title, gridPos, all_targets, datasource)
panels.extend([t_in, t_eg])
if 'EVERYSINGLETARGET' in remotes:
del remotes['EVERYSINGLETARGET']
for remote in remotes:
_in, _out = create_aggregate_panel(
title + f' - {remote}', gridPos, remotes[remote], datasource)
panels.extend([_in, _out])
result = {
'title': title,
'datasource': datasource,
'panels': panels,
}
if isinstance(tag, list):
result['tags'] = tag
else:
result['tag'] = tag
return result
def get_nren_dashboard_data_single(data, datasource, tag): def get_nren_dashboard_data_single(data, datasource, tag):
...@@ -790,10 +887,6 @@ def get_nren_dashboard_data_single(data, datasource, tag): ...@@ -790,10 +887,6 @@ def get_nren_dashboard_data_single(data, datasource, tag):
return result return result
def get_nren_dashboard_data(data, datasource, tag):
yield from _get_dashboard_data(data, datasource, tag, get_nren_dashboard_data_single)
def get_re_peer_dashboard_data_single(data, datasource, tag): def get_re_peer_dashboard_data_single(data, datasource, tag):
""" """
Helper for generating dashboard definitions for a single R&E Peer. Helper for generating dashboard definitions for a single R&E Peer.
...@@ -863,10 +956,6 @@ def get_re_peer_dashboard_data_single(data, datasource, tag): ...@@ -863,10 +956,6 @@ def get_re_peer_dashboard_data_single(data, datasource, tag):
return result return result
def get_re_peer_dashboard_data(data, datasource, tag):
yield from _get_dashboard_data(data, datasource, tag, get_re_peer_dashboard_data_single)
def get_service_dashboard_data_single(data, datasource, tag): def get_service_dashboard_data_single(data, datasource, tag):
""" """
Helper for generating dashboard definitions for a single service. Helper for generating dashboard definitions for a single service.
...@@ -917,10 +1006,6 @@ def get_service_dashboard_data_single(data, datasource, tag): ...@@ -917,10 +1006,6 @@ def get_service_dashboard_data_single(data, datasource, tag):
return result return result
def get_service_dashboard_data(data, datasource, tag):
yield from _get_dashboard_data(data, datasource, tag, get_service_dashboard_data_single)
def get_dashboard_data_single( def get_dashboard_data_single(
data, datasource, tag, data, datasource, tag,
panel_generator=default_interface_panel_generator, panel_generator=default_interface_panel_generator,
...@@ -963,8 +1048,7 @@ def get_dashboard_data( ...@@ -963,8 +1048,7 @@ def get_dashboard_data(
panel_generator=default_interface_panel_generator, panel_generator=default_interface_panel_generator,
errors=False): errors=False):
""" """
Helper for generating dashboard definitions for all non-NREN dashboards. Helper for generating dashboard definitions for interface-based non-NREN dashboards.
Uses multiprocessing to speed up generation.
:param data: the dashboard names and the panel data for each dashboard :param data: the dashboard names and the panel data for each dashboard
:param datasource: datasource to use for the panels :param datasource: datasource to use for the panels
...@@ -976,135 +1060,36 @@ def get_dashboard_data( ...@@ -976,135 +1060,36 @@ def get_dashboard_data(
:return: generator for dashboard definitions for each dashboard :return: generator for dashboard definitions for each dashboard
""" """
with ProcessPoolExecutor(max_workers=NUM_PROCESSES) as executor: func = partial(
try: get_dashboard_data_single,
for dash in executor.map( datasource=datasource, tag=tag, panel_generator=panel_generator, errors=errors)
partial(
get_dashboard_data_single,
datasource=datasource,
tag=tag,
panel_generator=panel_generator,
errors=errors),
data.items()
):
yield dash
finally:
executor.shutdown(wait=False)
def create_aggregate_panel(title, gridpos, targets, datasource):
"""
Helper for generating aggregate panels. Creates two panels, one for
ingress and one for egress.
:param title: title for the panel
:param gridpos: generator for grid position
:param targets: list of targets for the panels, used to build separate
targets for both ingress and egress.
:param datasource: datasource to use for the panels
:return: tuple of aggregate panels, one for ingress and one for egress
"""
ingress_targets, egress_targets = get_aggregate_targets(targets)
ingress_pos = next(gridpos) yield from map(func, data.items())
egress_pos = next(gridpos)
is_total = 'totals' in title.lower()
def reduce_alias(prev, curr): def get_nren_dashboard_data(data, datasource, tag):
alias = curr['alias']
if 'egress' in alias.lower():
prev[alias] = '#0000FF'
else:
prev[alias] = '#00FF00'
return prev
ingress_colors = reduce(reduce_alias, ingress_targets, {})
egress_colors = reduce(reduce_alias, egress_targets, {})
ingress = create_panel(
**ingress_pos,
stack=True,
linewidth=0 if is_total else 1,
datasource=datasource,
title=title + " - ingress",
targets=ingress_targets,
y_axis_type="bits",
alias_colors=ingress_colors if is_total else {},
)
egress = create_panel( func = partial(
**egress_pos, get_nren_dashboard_data_single,
stack=True,
linewidth=0 if is_total else 1,
datasource=datasource, datasource=datasource,
title=title + " - egress", tag=tag)
targets=egress_targets,
y_axis_type="bits",
alias_colors=egress_colors if is_total else {},
)
return ingress, egress yield from map(func, data.items())
def get_aggregate_dashboard_data(title, remotes, datasource, tag): def get_re_peer_dashboard_data(data, datasource, tag):
""" func = partial(
Helper for generating aggregate dashboard definitions. get_re_peer_dashboard_data_single,
Aggregate dashboards consist only of aggregate panels that are datasource=datasource,
panels with data for multiple interfaces. tag=tag)
At the top of the dashboard are two aggregate panels showing
total ingress and egress data for all interfaces.
Below that are two aggregate panels for each target, one for
ingress and one for egress.
:param title: title for the dashboard
:param remotes: dictionary of targets for the panels, the key is the
remote (usually a customer) and the value is a list of targets
for that remote. A single target represents how to fetch
data for one interface.
:param datasource: datasource to use for the panels
:param tag: tag to use for the dashboard, used for dashboard dropdowns on
the home dashboard.
:return: dashboard definition for the aggregate dashboard
"""
id_gen = num_generator()
gridPos = gridPos_generator(id_gen, agg=True)
panels = []
all_targets = remotes.get('EVERYSINGLETARGET', [])
ingress, egress = create_aggregate_panel(
title, gridPos, all_targets, datasource)
panels.extend([ingress, egress])
totals_title = title + ' - Totals' yield from map(func, data.items())
t_in, t_eg = create_aggregate_panel(
totals_title, gridPos, all_targets, datasource)
panels.extend([t_in, t_eg])
if 'EVERYSINGLETARGET' in remotes:
del remotes['EVERYSINGLETARGET']
for remote in remotes: def get_service_dashboard_data(data, datasource, tag):
_in, _out = create_aggregate_panel( func = partial(
title + f' - {remote}', gridPos, remotes[remote], datasource) get_service_dashboard_data_single,
panels.extend([_in, _out]) datasource=datasource,
tag=tag)
result = {
'title': title,
'datasource': datasource,
'panels': panels,
}
if isinstance(tag, list):
result['tags'] = tag
else:
result['tag'] = tag
return result yield from map(func, data.items())
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment