diff --git a/brian_dashboard_manager/grafana/dashboard.py b/brian_dashboard_manager/grafana/dashboard.py index 5b11704a675e52d603064b025e7ac589fe8a100c..012ccad4b8b783393c7d8162b1bee2d606fd6a4e 100644 --- a/brian_dashboard_manager/grafana/dashboard.py +++ b/brian_dashboard_manager/grafana/dashboard.py @@ -74,12 +74,27 @@ def delete_dashboards(request: TokenRequest): # Searches for a dashboard with given title -def find_dashboard(request: TokenRequest, title): - r = request.get('api/search', params={ - 'query': title - }) +def find_dashboard(request: TokenRequest, title=None): + param = { + **({'query': title} if title else {}), + 'type': 'dash-db', + 'limit': 5000, + 'page': 1 + } + r = request.get('api/search', params=param) if r and len(r) > 0: - return r[0] + if title: + return r[0] + else: + while True: + param['page'] += 1 + page = request.get('api/search', params=param) + if len(page) > 0: + r.extend(page) + else: + break + return r + return None diff --git a/brian_dashboard_manager/grafana/provision.py b/brian_dashboard_manager/grafana/provision.py index 80b5aae2c1ddae3e4df5c21017a117b0b17abe24..54b5bf90391a38e9cf48f87e5610f0bbba0ee793 100644 --- a/brian_dashboard_manager/grafana/provision.py +++ b/brian_dashboard_manager/grafana/provision.py @@ -7,6 +7,7 @@ import logging import time import json import datetime +from functools import reduce from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor from brian_dashboard_manager.config import DEFAULT_ORGANIZATIONS, STATE_PATH from brian_dashboard_manager.grafana.utils.request import \ @@ -15,7 +16,7 @@ from brian_dashboard_manager.grafana.utils.request import \ from brian_dashboard_manager.grafana.organization import \ get_organizations, create_organization, create_api_token, \ delete_api_token, delete_expired_api_tokens, set_home_dashboard -from brian_dashboard_manager.grafana.dashboard import \ +from brian_dashboard_manager.grafana.dashboard import find_dashboard, \ get_dashboard_definitions, create_dashboard, delete_dashboard from brian_dashboard_manager.grafana.datasource import \ check_provisioned, create_datasource @@ -39,10 +40,13 @@ logger = logging.getLogger(__name__) def generate_all_nrens(token_request, nrens, folder_id, datasource_name): + provisioned = [] with ThreadPoolExecutor(max_workers=8) as executor: for dashboard in generate_nrens(nrens, datasource_name): - executor.submit(create_dashboard, token_request, - dashboard, folder_id) + res = executor.submit(create_dashboard, token_request, + dashboard, folder_id) + provisioned.append(res) + return [r.result() for r in provisioned] def provision_folder(token_request, folder_name, @@ -70,6 +74,8 @@ def provision_folder(token_request, folder_name, excluded_dashboards = list( map(lambda s: s.lower(), excluded_dashboards)) + provisioned = [] + with ThreadPoolExecutor(max_workers=4) as executor: for dashboard in dash_data: rendered = render_dashboard(dashboard) @@ -77,8 +83,9 @@ def provision_folder(token_request, folder_name, executor.submit(delete_dashboard, token_request, rendered, folder['id']) continue - executor.submit(create_dashboard, token_request, - rendered, folder['id']) + provisioned.append(executor.submit(create_dashboard, token_request, + rendered, folder['id'])) + return [r.result() for r in provisioned] def provision_aggregate(token_request, agg_type, aggregate_folder, @@ -93,7 +100,7 @@ def provision_aggregate(token_request, agg_type, aggregate_folder, f'Aggregate - {agg_type}', data, datasource_name, tag) rendered = render_dashboard(dashboard) - create_dashboard(token_request, rendered, aggregate_folder['id']) + return create_dashboard(token_request, rendered, aggregate_folder['id']) def provision_maybe(config): @@ -247,7 +254,17 @@ def provision(config): datasource_name = datasource.get('name', 'PollerInfluxDB') excluded_folders = org_config.get('excluded_folders', {}) + def get_uid(prev, curr): + prev[curr.get('uid')] = False + return prev + + # Map of dashboard UID -> whether it has been updated. + # This is used to remove stale dashboards at the end. + dash_list = find_dashboard(token_request) or [] + dash_list = reduce(get_uid, dash_list, {}) + with ProcessPoolExecutor(max_workers=4) as executor: + provisioned = [] for folder_name, dash in dashboards.items(): exclude = excluded_folders.get(folder_name) if exclude: @@ -260,10 +277,19 @@ def provision(config): logger.info( f'Provisioning {org["name"]}/{folder_name} dashboards') - executor.submit(provision_folder, token_request, - folder_name, dash, - excluded_interfaces, datasource_name, - exclude) + res = executor.submit(provision_folder, token_request, + folder_name, dash, + excluded_interfaces, datasource_name, + exclude) + provisioned.append(res) + for result in provisioned: + folder = result.result() + if folder is None: + continue + for dashboard in folder: + if dashboard is None: + continue + dash_list[dashboard.get('uid')] = True aggregate_dashboards = { 'CLS PEERS': { @@ -296,6 +322,7 @@ def provision(config): pass else: with ProcessPoolExecutor(max_workers=4) as executor: + provisioned = [] agg_folder = find_folder(token_request, 'Aggregates') for agg_type, dash in aggregate_dashboards.items(): if agg_type in exclude_agg: @@ -306,28 +333,40 @@ def provision(config): continue logger.info(f'Provisioning {org["name"]}' + f'/Aggregate {agg_type} dashboards') - executor.submit(provision_aggregate, token_request, - agg_type, agg_folder, dash, - excluded_interfaces, datasource_name) + res = executor.submit(provision_aggregate, token_request, + agg_type, agg_folder, dash, + excluded_interfaces, datasource_name) + provisioned.append(res) + + for result in provisioned: + dashboard = result.result() + if dashboard is None: + continue + dash_list[dashboard.get('uid')] = True # NREN Access dashboards # uses a different template than the above. logger.info('Provisioning NREN Access dashboards') - # always recreate NREN folder - delete_folder(token_request, 'NREN Access') folder = find_folder(token_request, 'NREN Access') nrens = filter(is_nren, excluded_interfaces) - generate_all_nrens(token_request, - nrens, folder['id'], datasource_name) + provisioned = generate_all_nrens( + token_request, nrens, folder['id'], datasource_name) + + for dashboard in provisioned: + if dashboard is None: + continue + dash_list[dashboard.get('uid')] = True # Non-generated dashboards excluded_dashboards = org_config.get('excluded_dashboards', []) logger.info('Provisioning static dashboards') for dashboard in get_dashboard_definitions(): if dashboard['title'] not in excluded_dashboards: - create_dashboard(token_request, dashboard) + res = create_dashboard(token_request, dashboard) + if res: + dash_list[res.get('uid')] = True else: delete_dashboard(token_request, dashboard) @@ -336,6 +375,13 @@ def provision(config): logger.info('Configuring Home dashboard') is_staff = org['name'] == 'GÉANT Staff' set_home_dashboard(token_request, is_staff) + # just hardcode that we updated home dashboard + dash_list['home'] = True + + for dash, provisioned in dash_list.items(): + if not provisioned: + logger.info(f'Deleting stale dashboard with UID {dash}') + delete_dashboard(token_request, {'uid': dash}) logger.info(f'Time to complete: {time.time() - start}') for org_id, token in tokens: diff --git a/changelog.md b/changelog.md index 4ffeb7a57a474be8a1dd380b65ef7e33d6d5935a..ce622e4126e6cd8b35f3823e6ff3a6639eae63bb 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. +## [0.14] - 2021-04-19 +- [POL1-420] Delete stale dashboards after provisioning. + ## [0.13] - 2021-04-15 - Delete NREN folder when provisioning, stale dashboards could otherwise never be removed. diff --git a/setup.py b/setup.py index eb68a217263496bee12adb255450881ad5686e2e..7a3fea90c63b1e8ce54e678688035d1c4fc5a2bd 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name='brian-dashboard-manager', - version="0.13", + version="0.14", author='GEANT', author_email='swd@geant.org', description='', diff --git a/test/test_update.py b/test/test_update.py index 04851d0a157642b4be79ff0ead7c1581c8e72463..0512ebcebc934133fb3f53fadcc2ad46f0b990e0 100644 --- a/test/test_update.py +++ b/test/test_update.py @@ -1,6 +1,7 @@ import responses import json import re +from brian_dashboard_manager.grafana.utils.request import TokenRequest from brian_dashboard_manager.templating.nren_access import get_nrens from brian_dashboard_manager.grafana.provision import provision_folder, \ generate_all_nrens, provision @@ -241,6 +242,7 @@ def test_provision_folder(data_config, mocker): 'testdatasource', ['CLS TESTDASHBOARD']) +@responses.activate def test_provision_nrens(data_config, mocker): NREN_INTERFACES = [ # physical @@ -299,12 +301,34 @@ def test_provision_nrens(data_config, mocker): } ] + UID = '1337' + + def get_callback(request): + query = request.params.get('query') + return 200, {}, json.dumps({'uid': UID, 'title': query}) + + responses.add_callback( + method=responses.GET, + url=re.compile(f"http://{data_config['hostname']}/api/search"), + callback=get_callback) + + def post_callback(request): + dashboard = json.loads(request.body).get('dashboard') + title = dashboard.get('title') + return 200, {}, json.dumps({'uid': UID, 'title': title}) + + responses.add_callback( + method=responses.POST, + url=re.compile(f"http://{data_config['hostname']}/api/dashboards/db"), + callback=post_callback) + nrens = get_nrens(NREN_INTERFACES) assert len(nrens) == 1 and nrens.get('HEANET') is not None assert len(nrens.get('HEANET').get('AGGREGATES')) == 1 assert len(nrens.get('HEANET').get('SERVICES')) == 1 assert len(nrens.get('HEANET').get('PHYSICAL')) == 2 - generate_all_nrens(None, NREN_INTERFACES, 1, 'testdatasource') + token_request = TokenRequest(token='testtoken', **data_config) + generate_all_nrens(token_request, NREN_INTERFACES, 1, 'testdatasource') @responses.activate