diff --git a/.gitignore b/.gitignore index afd19c6e003dfddc99985617debd4b7845c0bd45..e2a3e9346c070336a189a33c39c09130942da8fe 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ htmlcov .tox dist docs/build +.coverage* # logs *.log diff --git a/brian_dashboard_manager/grafana/organization.py b/brian_dashboard_manager/grafana/organization.py index 629fe8a956187f021470fa3a610a3116e63b0246..eb2313f702582f60f40bbb6c21b89009bb9f5c7f 100644 --- a/brian_dashboard_manager/grafana/organization.py +++ b/brian_dashboard_manager/grafana/organization.py @@ -6,6 +6,7 @@ Grafana Organization management helpers. import logging import random import string +from requests.exceptions import HTTPError from datetime import datetime from typing import Dict, List, Union @@ -136,6 +137,78 @@ def delete_expired_api_tokens(request: AdminRequest) -> bool: return True +def get_or_create_service_account(request: AdminRequest, org_id): + """ + Gets a service account for the given organization, or creates one if it does not exist. + + :param request: AdminRequest object + :param org_id: organization ID + :param name: service account name + :return: service account definition + """ + switch_active_organization(request, org_id) + + # get provision service account, if it exists + try: + service_accounts = request.get('api/serviceaccounts?perpage=10&page=1&query=provision').json() + + if service_accounts and service_accounts.get('totalCount') > 0: + service_account = service_accounts.get('serviceAccounts')[0] + return service_account + except HTTPError as e: + if e.response.status_code != 404: + raise e + + # create a service account for provisioning + try: + result = request.post( + 'api/serviceaccounts', json={ + 'name': 'provision', + 'role': 'Admin', + 'isDisabled': False, + }).json() + except HTTPError as e: + print(e) + + logger.info(f'Created provision service account for organization #{org_id}') + return result + + +def create_service_account_token(request: AdminRequest, service_account_id: int): + """ + Creates a new API token for the given service account. + + :param request: AdminRequest object + :param service_account_id: service account ID + :return: Token definition + """ + data = { + 'name': f'provision-token-{datetime.now().isoformat()}', + } + + result = request.post(f'api/serviceaccounts/{service_account_id}/tokens', json=data).json() + token_id = result.get('id') + + logger.debug(f'Created API token #{token_id} for service account #{service_account_id}') + + return result + + +def delete_service_account(request: AdminRequest, service_account_id: int): + """ + Deletes a service account with the given ID. + + :param request: AdminRequest object + :param service_account_id: service account ID + :return: delete response + """ + + assert service_account_id is not None + result = request.delete(f'api/serviceaccounts/{service_account_id}') + logger.debug(f'Deleted service account #{service_account_id}') + return result + + def set_home_dashboard(request: TokenRequest, is_staff): """ Sets the home dashboard for the organization diff --git a/brian_dashboard_manager/grafana/provision.py b/brian_dashboard_manager/grafana/provision.py index 5d9fae1a559455c706ba4bdb24b3b29f4f64b0e2..b9ce5bf75380851e942d6df1864780127818d410 100644 --- a/brian_dashboard_manager/grafana/provision.py +++ b/brian_dashboard_manager/grafana/provision.py @@ -18,7 +18,8 @@ from brian_dashboard_manager.services.api import fetch_services from brian_dashboard_manager.grafana.organization import \ get_organizations, create_organization, create_api_token, \ - delete_api_token, delete_expired_api_tokens, set_home_dashboard + delete_api_token, delete_expired_api_tokens, set_home_dashboard, \ + get_or_create_service_account, delete_service_account, create_service_account_token from brian_dashboard_manager.grafana.dashboard import list_dashboards, \ get_dashboard_definitions, create_dashboard, delete_dashboard from brian_dashboard_manager.grafana.datasource import \ @@ -814,10 +815,13 @@ def provision(config, raise_exceptions=False): """ start = time.time() - tokens = [] + accounts = [] all_orgs = _provision_orgs(config) request = AdminRequest(**config) - delete_expired_api_tokens(request) + try: + delete_expired_api_tokens(request) + except Exception: + pass # needed for older versions of grafana def _find_org_config(org): orgs_to_provision = config.get('organizations', DEFAULT_ORGANIZATIONS) @@ -840,10 +844,17 @@ def provision(config, raise_exceptions=False): # message logged from _find_org_config continue - token = create_api_token(request, org_id) + try: + token = create_api_token(request, org_id) + accounts.append((org_id, token)) + except Exception: + # create a service account for provisioning (>grafana 11.0) + account = get_or_create_service_account(request, org_id) + token = create_service_account_token(request, account['id']) + accounts.append((org_id, account)) + token_request = TokenRequest(token=token['key'], **config) - tokens.append((org_id, token['id'])) - logger.debug(tokens) + logger.debug(accounts) all_original_dashboards = list_dashboards(token_request) all_original_dashboard_uids = { @@ -902,7 +913,11 @@ def provision(config, raise_exceptions=False): folders_to_keep.update(ignored_folders) delete_unknown_folders(token_request, folders_to_keep) - delete_api_token(request, token['id'], org_id=org_id) + try: + delete_api_token(request, token['id'], org_id=org_id) + except Exception: + # we're on a newer version of grafana + delete_service_account(request, account['id']) except Exception: logger.exception(f'Error when provisioning org {org["name"]}') if raise_exceptions: diff --git a/brian_dashboard_manager/templating/render.py b/brian_dashboard_manager/templating/render.py index ac5b847b2ac9ebcd4e6f9b49c606ddf32d2853e6..8671e15421a1ccb18e3fd4d3d219c3133fa69013 100644 --- a/brian_dashboard_manager/templating/render.py +++ b/brian_dashboard_manager/templating/render.py @@ -262,11 +262,17 @@ def create_panel( def create_infobox(): return { "datasource": None, - "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}, + "gridPos": {"h": 2, "w": 24, "x": 0, "y": 0}, "id": 1, - "options": {"content": "", "mode": "html"}, + "options": { + "content": """ + <center style="margin-top:5px;"> + INFO: The average values displayed are only mean values for timescales of 2 days or less + </center>""", + "mode": "html" + }, "pluginVersion": "8.2.5", - "title": "INFO: The average values displayed are only mean values for timescales of 2 days or less", + "title": "", "type": "text", } diff --git a/changelog.md b/changelog.md index 9da8f5c7de19168367e0deccc50af0500763957d..6eb8973c76359b0f674658976bcbbf618644d9e9 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. +## [0.65] - 2024-09-23 +- Support Grafana v11.3 layout & API changes + ## [0.64] - 2024-08-14 - POL1-841 - Promote NREN Access Beta and keep Legacy dashboard for GEANT staff diff --git a/setup.py b/setup.py index bad5488d8f9a33095612be037fee6e7e925dbb1b..be5db1ef6741eb993ae6a217bcbe2aafb3baaa7a 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name='brian-dashboard-manager', - version="0.64", + version="0.65", author='GEANT', author_email='swd@geant.org', description='', diff --git a/test/test_update.py b/test/test_update.py index f4ab22416f295a72d9960a489f2e79b046ab6625..16800bf946b492b745e0d13ddabf1f365620e25a 100644 --- a/test/test_update.py +++ b/test/test_update.py @@ -778,7 +778,7 @@ def test_provision_re_peer_dashboard( panels = mock_grafana.dashboards_by_folder_uid[folder_uid][0]["panels"] expected_types = ["text", "graph", "graph", "row", "graph", "graph", "row"] assert [p["type"] for p in panels] == expected_types - assert "INFO" in panels[0]["title"] + assert "INFO" in panels[0]["options"]["content"] assert "ingress" in panels[1]["title"] assert "egress" in panels[2]["title"] assert "Services" in panels[3]["title"]