diff --git a/brian_dashboard_manager/grafana/provision.py b/brian_dashboard_manager/grafana/provision.py index 0259d48414956946ce6b2f990af970e0a195e1ed..51ff6ee4bd8726c770f760afb511aff3ce56264a 100644 --- a/brian_dashboard_manager/grafana/provision.py +++ b/brian_dashboard_manager/grafana/provision.py @@ -33,7 +33,8 @@ from brian_dashboard_manager.templating.helpers import \ get_aggregate_dashboard_data, get_interface_data, \ get_nren_interface_data, get_dashboard_data, \ get_nren_dashboard_data, get_aggregate_interface_data, \ - get_nren_interface_data_old, get_re_peer_dashboard_data, get_re_peer_interface_data + get_nren_interface_data_old, get_re_peer_dashboard_data, get_re_peer_interface_data, get_service_data, \ + get_service_dashboard_data from brian_dashboard_manager.templating.gws import generate_gws, generate_indirect from brian_dashboard_manager.templating.eumetsat import generate_eumetsat_multicast @@ -136,6 +137,16 @@ DASHBOARDS = { } } +SERVICE_DASHBOARDS = { + 'MWS': { + 'tag': ['mws'], + 'service_type': 'GEANT MANAGED WAVELENGTH SERVICE', + 'folder_name': 'Managed Wavelength Service', + 'interfaces': [], + 'services': [] + } +} + AGG_DASHBOARDS = { 'CLS_PEERS': { 'tag': 'cls_peers', @@ -181,8 +192,8 @@ AGG_DASHBOARDS = { } -def provision_folder(token_request, folder_name, dash, - config, ds_name, excluded_dashboards): +def provision_folder(token_request, folder_name, dash, services, + ds_name, excluded_dashboards): """ Function to provision dashboards within a folder. @@ -190,7 +201,7 @@ def provision_folder(token_request, folder_name, dash, :param folder_name: Name of the folder to provision dashboards in :param dash: the dashboards to provision, with interface data to generate the dashboards from - :param config: the application config + :param services: service data from reporting provider for service-based dashboards :param ds_name: the name of the datasource to query in the dashboard panels :param excluded_dashboards: list of dashboards to exclude from provisioning for the organisation @@ -218,19 +229,22 @@ def provision_folder(token_request, folder_name, dash, is_nren_beta = folder_name == "NREN Access BETA" # needed for POL1-642 BETA is_nren = folder_name == "NREN Access" is_re_peer = folder_name == "RE Peer" + is_service = 'service_type' in dash if is_nren: data = get_nren_interface_data_old(interfaces) dash_data = get_nren_dashboard_data(data, ds_name, tag) elif is_nren_beta: # needed for POL1-642 BETA - services = fetch_services(config['reporting_provider']) data = get_nren_interface_data( services, interfaces, excluded_dashboards) dash_data = get_nren_dashboard_data(data, ds_name, tag) elif is_re_peer: data = get_re_peer_interface_data(interfaces) dash_data = get_re_peer_dashboard_data(data, ds_name, tag) + elif is_service: + data = get_service_data(dash['service_type'], services, interfaces, excluded_dashboards) + dash_data = get_service_dashboard_data(data, ds_name, tag) else: data = get_interface_data(interfaces) dash_data = get_dashboard_data( @@ -243,7 +257,7 @@ def provision_folder(token_request, folder_name, dash, with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: for dashboard in dash_data: - if is_nren or is_nren_beta or is_re_peer: + if is_nren or is_nren_beta or is_re_peer or is_service: rendered = render_complex_dashboard(**dashboard) else: rendered = render_simple_dashboard(**dashboard) @@ -346,6 +360,20 @@ def excluded_folder_dashboards(org_config, folder_name): return excluded if isinstance(excluded, list) else [] +def _interfaces_to_keep(interface, excluded_nrens): + dash_info = interface.get('dashboards_info') + if dash_info is None: + logger.info(f'No "dashboards_info" for ' + f'{interface["router"]}:{interface["name"]}') + # throw it away + return False + dashboards = {nren['name'].lower() for nren in dash_info} + is_lab_router = 'lab.office' in interface['router'].lower() + should_keep = not (is_lab_router or any( + nren.lower() in dashboards for nren in excluded_nrens)) + return should_keep + + def _provision_interfaces(config, org_config, ds_name, token): """ This function is used to provision most dashboards, @@ -359,24 +387,12 @@ def _provision_interfaces(config, org_config, ds_name, token): """ interfaces = get_interfaces(config['inventory_provider']) + services = fetch_services(config['reporting_provider']) excluded_nrens = org_config['excluded_nrens'] excluded_folders = org_config.get('excluded_folders', {}) - def interfaces_to_keep(interface): - dash_info = interface.get('dashboards_info') - if dash_info is None: - logger.info(f'No "dashboards_info" for ' - f'{interface["router"]}:{interface["name"]}') - # throw it away - return False - dashboards = {nren['name'].lower() for nren in dash_info} - is_lab_router = 'lab.office' in interface['router'].lower() - should_keep = not (is_lab_router or any( - nren.lower() in dashboards for nren in excluded_nrens)) - return should_keep - - relevant_interfaces = list(filter(interfaces_to_keep, interfaces)) + relevant_interfaces = list(filter(lambda x: _interfaces_to_keep(x, excluded_nrens), interfaces)) for interface in relevant_interfaces: interface['dashboards_info'] = list(filter( lambda x: x['name'] != '', @@ -429,7 +445,7 @@ def _provision_interfaces(config, org_config, ds_name, token): f'Provisioning {org_config["name"]}/{folder_name} dashboards') res = executor.submit( provision_folder, token, - folder_name, folder, config, ds_name, + folder_name, folder, services, ds_name, excluded_folder_dashboards(org_config, folder_name)) provisioned.append(res) @@ -588,6 +604,83 @@ def _provision_aggregates(config, org_config, ds_name, token): yield from provisioned +def _provision_service_dashboards(config, org_config, ds_name, token): + """ + This function is used to provision service-specific dashboards, + overwriting existing ones. + + :param config: the application config + :param org_config: the organisation config + :param ds_name: the name of the datasource to query in the dashboards + :param token: a token_request object + :return: generator of UIDs of dashboards that were created + """ + interfaces = get_interfaces(config['inventory_provider']) + services = fetch_services(config['reporting_provider']) + + excluded_nrens = org_config['excluded_nrens'] + excluded_folders = org_config.get('excluded_folders', {}) + + logger.info('Provisioning service-specific dashboards') + + relevant_interfaces = list(filter(lambda x: _interfaces_to_keep(x, excluded_nrens), interfaces)) + for interface in relevant_interfaces: + interface['dashboards_info'] = list(filter( + lambda x: x['name'] != '', + interface['dashboards_info'] + )) + + # create a lookup index for interface data + interfaces_index = {f'{interface["router"]}###{interface["name"]}': interface for interface in relevant_interfaces} + # loop over service dashboards and get service types we care about + dash_service_types = {SERVICE_DASHBOARDS[dash]['service_type']: dash for dash in SERVICE_DASHBOARDS} + # loop over services and get relevant interfaces, append to dashboards + for service in services: + if service['service_type'] in dash_service_types: + dash = dash_service_types[service['service_type']] + svcs = SERVICE_DASHBOARDS[dash]['services'] + svcs.append(service) + # attach service interfaces + if len(service['endpoints']) == 0: + continue + ifaces = SERVICE_DASHBOARDS[dash]['interfaces'] + for endpoint in service['endpoints']: + if 'interface' in endpoint: + if_name = endpoint.get('interface') + router = endpoint.get('hostname') + ifc = interfaces_index.get(f'{router}###{if_name}') + if ifc: + ifaces.append(ifc) + + # provision dashboards and their folders + with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: + provisioned = [] + for folder in SERVICE_DASHBOARDS.values(): + folder_name = folder['folder_name'] + + # boolean True means entire folder excluded + # if list, it is specific dashboard names not to provision + # so is handled at provision time. + if is_excluded_folder(excluded_folders, folder_name): + executor.submit( + delete_folder, token, title=folder_name) + continue + + logger.info( + f'Provisioning {org_config["name"]}/{folder_name} dashboards') + res = executor.submit( + provision_folder, token, + folder_name, folder, services, ds_name, + excluded_folder_dashboards(org_config, folder_name)) + provisioned.append(res) + + for result in provisioned: + folder = result.result() + if folder is None: + continue + yield from folder + + def _provision_static_dashboards(config, org_config, ds_name, token): """ This function is used to provision static dashboards from json files, @@ -795,6 +888,8 @@ def provision(config, raise_exceptions=False): config, org_config, ds_name, token_request), _provision_aggregates( config, org_config, ds_name, token_request), + _provision_service_dashboards( + config, org_config, ds_name, token_request), _provision_static_dashboards( config, org_config, ds_name, token_request), _get_ignored_dashboards( @@ -821,7 +916,8 @@ def provision(config, raise_exceptions=False): 'GWS Indirect', 'GWS Direct', 'Aggregates', - 'EUMETSAT Multicast' + 'EUMETSAT Multicast', + 'Managed Wavelength Service', } folders_to_keep.update({dash['folder_name'] for dash in DASHBOARDS.values()}) diff --git a/brian_dashboard_manager/templating/helpers.py b/brian_dashboard_manager/templating/helpers.py index 31c47cf52313f91c65792d1aee8b030f3754e038..7061ec6ff303182f4ac0d4140c2d0f68140ff4f9 100644 --- a/brian_dashboard_manager/templating/helpers.py +++ b/brian_dashboard_manager/templating/helpers.py @@ -340,7 +340,7 @@ def get_nren_interface_data(services, interfaces, excluded_dashboards): 'interface': interface_name, 'hostname': host, 'alias': - f"{router} - {interface_name} - {dashboard_name} " + f"{router} - {interface_name} - {dashboard_name} " }) if info['interface_type'] == 'AGGREGATE': @@ -368,6 +368,120 @@ def get_nren_interface_data(services, interfaces, excluded_dashboards): return result +def get_service_data(service_type, services, interfaces, excluded_dashboards): + """ + Helper for grouping interface data to be used for generating + dashboards for specific service types, grouped by NRENs. + + Extracts information from interfaces to be used in panels. + + :param services: list of services + :param interfaces: list of interfaces + :param excluded_dashboards: list of dashboards to exclude for + the organization we are generating dashboards for + + :return: dictionary of dashboards and their service/interface data + """ + result = {} + + customers = defaultdict(list) + + for service in services: + _customers = service.get('customers') + _service_type = service.get('service_type') + for cust in _customers: + if cust.lower() in excluded_dashboards: + continue + if _service_type != service_type: + continue + customers[cust].append(service) + + for customer, services in customers.items(): + dashboard = result.setdefault(customer, { + 'SERVICES': [], + 'PHYSICAL': [] + }) + + for service in services: + _interfaces = service.get('endpoints') + name = service.get('name') + sid = service.get('sid') + scid = service.get('scid') + + measurement = 'scid_rates' + + if len(_interfaces) == 0: + continue + + if 'interface' in _interfaces[0]: + if_name = _interfaces[0].get('interface') + router = _interfaces[0].get('hostname') + else: + if_name = _interfaces[0].get('port') + router = _interfaces[0].get('equipment') + router = router.replace('.geant.net', '') + title = f'{router} - {{}} - {if_name} - {name} ({sid})' + + has_v6_interface = False + for interface in _interfaces: + if 'addresses' in interface: + for address in interface['addresses']: + if address.find(':') > 0: + has_v6_interface = True + break + + dashboard['SERVICES'].append({ + 'measurement': measurement, + 'title': title, + 'scid': scid, + 'sort': (sid[:2], name), + 'has_v6': has_v6_interface + }) + + for interface in interfaces: + + description = interface['description'].strip() + interface_name = interface['name'] + host = interface['router'] + + router = host.replace('.geant.net', '') + panel_title = f"{router} - {{}} - {interface_name} - {description}" + + dashboards_info = interface['dashboards_info'] + + for info in dashboards_info: + dashboard_name = info['name'] + + dashboard = result.get(dashboard_name, { + 'SERVICES': [], + 'PHYSICAL': [] + }) + + if info['interface_type'] == 'AGGREGATE': + # link aggregates are also shown + # under the physical dropdown + dashboard['PHYSICAL'].append({ + 'title': panel_title, + 'hostname': host, + 'interface': interface_name + }) + elif info['interface_type'] == 'PHYSICAL': + dashboard['PHYSICAL'].append({ + 'title': panel_title, + 'hostname': host, + 'interface': interface_name + }) + + result[dashboard_name] = dashboard + + for customer in list(result.keys()): + lengths = [len(val) for val in result[customer].values()] + if sum(lengths) == 0: + # no services/interfaces, so remove it + del result[customer] + return result + + def get_interface_data(interfaces): """ Helper for grouping interface data to be used for generating @@ -438,6 +552,7 @@ def get_aggregate_interface_data(interfaces, agg_name, group_field): prev[curr[field]] = groups prev['EVERYSINGLETARGET'] = all_agg return prev + return reduce_func for interface in interfaces: @@ -612,6 +727,31 @@ def default_interface_panel_generator(gridPos, use_all_traffic=True, use_ipv6=Tr return get_panel_definitions +def _get_dashboard_data(data, datasource, tag, single_data_func): + """ + Helper for generating dashboard definitions. + Uses multiprocessing to speed up generation. + + :param data: the dashboard names and the panel data for each dashboard + :param datasource: datasource to use for the panels + :param tag: tag to use for the dashboard, used for dashboard dropdowns on + the home dashboard. + :param single_data_func: function that gets data for one definition + + :return: generator for dashboard definitions for each dashboard + """ + + with ProcessPoolExecutor(max_workers=NUM_PROCESSES) as executor: + for dash in executor.map( + partial( + single_data_func, + datasource=datasource, + tag=tag), + data.items() + ): + yield dash + + def get_nren_dashboard_data_single(data, datasource, tag): """ Helper for generating dashboard definitions for a single NREN. @@ -649,7 +789,7 @@ def get_nren_dashboard_data_single(data, datasource, tag): def sort_key(panel): sort = panel.get('sort') if not sort: - return 'ZZZ'+panel.get('hostname') # sort to end + return 'ZZZ' + panel.get('hostname') # sort to end return sort service_panels = panel_gen( @@ -692,28 +832,7 @@ def get_nren_dashboard_data_single(data, datasource, tag): def get_nren_dashboard_data(data, datasource, tag): - """ - Helper for generating dashboard definitions for all NRENs. - Uses multiprocessing to speed up generation. - - :param data: the NREN names and the panel data for each NREN - :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: generator for dashboard definitions for each NREN - """ - - with ProcessPoolExecutor(max_workers=NUM_PROCESSES) as executor: - for dash in executor.map( - partial( - get_nren_dashboard_data_single, - datasource=datasource, - tag=tag), - data.items() - ): - - yield dash + yield from _get_dashboard_data(data, datasource, tag, get_nren_dashboard_data_single) def get_re_peer_dashboard_data_single(data, datasource, tag): @@ -752,7 +871,7 @@ def get_re_peer_dashboard_data_single(data, datasource, tag): def sort_key(panel): sort = panel.get('sort') if not sort: - return 'ZZZ'+panel.get('hostname') # sort to end + return 'ZZZ' + panel.get('hostname') # sort to end return sort service_panels = panel_gen( @@ -786,28 +905,68 @@ def get_re_peer_dashboard_data_single(data, datasource, tag): 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): """ - Helper for generating dashboard definitions for all R&E Peers. - Uses multiprocessing to speed up generation. + Helper for generating dashboard definitions for a single service. - :param data: the names and the panel data for each R&E Peer + :param data: data for the dashboard, including the service name and + the panel data :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: generator for dashboard definitions for each R&E Peer + :return: dashboard definition for the service dashboard """ - with ProcessPoolExecutor(max_workers=NUM_PROCESSES) as executor: - for dash in executor.map( - partial( - get_re_peer_dashboard_data_single, - datasource=datasource, - tag=tag), - data.items() - ): + service_name, dash = data + id_gen = num_generator() - yield dash + gridPos = gridPos_generator(id_gen) + + panel_gen = default_interface_panel_generator(gridPos, use_all_traffic=True, use_ipv6=False) + + services_dropdown = create_dropdown_panel('Services', **next(gridPos)) + + def sort_key(panel): + sort = panel.get('sort') + if not sort: + return 'ZZZ' + panel.get('hostname') # sort to end + return sort + + service_panels = panel_gen( + sorted(dash['SERVICES'], key=sort_key), datasource) + + iface_dropdown = create_dropdown_panel('Interfaces', **next(gridPos)) + phys_panels = panel_gen(dash['PHYSICAL'], datasource, True) + + dropdown_groups = [{ + 'dropdown': services_dropdown, + 'panels': service_panels, + }] + dropdown_groups.append({ + 'dropdown': iface_dropdown, + 'panels': phys_panels, + }) + + result = { + 'nren_name': service_name, + 'datasource': datasource, + 'aggregate_panels': [], + 'dropdown_groups': dropdown_groups + } + if isinstance(tag, list): + result['tags'] = tag + else: + result['tag'] = tag + + 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( @@ -867,15 +1026,14 @@ def get_dashboard_data( with ProcessPoolExecutor(max_workers=NUM_PROCESSES) as executor: for dash in executor.map( - partial( - get_dashboard_data_single, - datasource=datasource, - tag=tag, - panel_generator=panel_generator, - errors=errors), - data.items() + partial( + get_dashboard_data_single, + datasource=datasource, + tag=tag, + panel_generator=panel_generator, + errors=errors), + data.items() ): - yield dash diff --git a/brian_dashboard_manager/templating/render.py b/brian_dashboard_manager/templating/render.py index 19b4cdfab4d7ac3d84fb0de47ad6d1d135f598ea..65dc9e41dd321714528e8d42985c02f72a216ca6 100644 --- a/brian_dashboard_manager/templating/render.py +++ b/brian_dashboard_manager/templating/render.py @@ -276,7 +276,8 @@ def render_complex_dashboard( ): assert tag or tags panels = [create_infobox()] - panels.extend(aggregate_panels) + if len(aggregate_panels) > 0: + panels.extend(aggregate_panels) for group in dropdown_groups: panels.append(group["dropdown"]) panels.extend(group["panels"])