diff --git a/CHANGELOG.md b/CHANGELOG.md index 2135ed1..cf26948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [1.4.0] - 2023-09-20 + +### Added +- add contact points and notifcation policy backup functionalities by @ysde in #238 +- added http headers to get_grafana_version request by @Mar8x in #239 + +### Changed +- added py3-packaging to slim docker image, reported by @tasiotas in #241 :tada: + +### Removed + # [1.3.3] - 2023-07-27 ### Added diff --git a/README.md b/README.md index 7ddb40e..a5e5063 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ There are three ways to setup the configuration: 3. Use `~/.grafana-backup.json` to define variables in json format. ### Example Config -* Check out the [examples](examples) folder for more configuration details +* Copy [grafanaSettings.example.json](examples/grafanaSettings.example.json) and modify it for you to use, remove `azure`, `aws`, `gcp`, `influxdb` blocks (but keep the ones you used). +* Check out the [examples](examples) folder for more configuration details. **NOTE** If you use `environment variables`, you need to add the following to your `.bashrc` or execute once before using the tool (please change variables according to your setup): diff --git a/examples/grafana-backup.example.json b/examples/grafanaSettings.example.json similarity index 100% rename from examples/grafana-backup.example.json rename to examples/grafanaSettings.example.json diff --git a/grafana_backup/api_checks.py b/grafana_backup/api_checks.py index 2c1cd96..490c889 100644 --- a/grafana_backup/api_checks.py +++ b/grafana_backup/api_checks.py @@ -1,5 +1,5 @@ from grafana_backup.commons import print_horizontal_line -from grafana_backup.dashboardApi import health_check, auth_check, uid_feature_check, paging_feature_check +from grafana_backup.dashboardApi import health_check, auth_check, uid_feature_check, paging_feature_check, contact_point_check def main(settings): @@ -12,25 +12,32 @@ def main(settings): api_auth_check = settings.get('API_AUTH_CHECK') if api_health_check: - (status, json_resp) = health_check(grafana_url, http_get_headers, verify_ssl, client_cert, debug) + (status, json_resp) = health_check(grafana_url, + http_get_headers, verify_ssl, client_cert, debug) if not status == 200: return (status, json_resp, None, None, None) if api_auth_check: - (status, json_resp) = auth_check(grafana_url, http_get_headers, verify_ssl, client_cert, debug) + (status, json_resp) = auth_check(grafana_url, + http_get_headers, verify_ssl, client_cert, debug) if not status == 200: return (status, json_resp, None, None, None) - dashboard_uid_support, datasource_uid_support = uid_feature_check(grafana_url, http_get_headers, verify_ssl, client_cert, debug) + dashboard_uid_support, datasource_uid_support = uid_feature_check( + grafana_url, http_get_headers, verify_ssl, client_cert, debug) if isinstance(dashboard_uid_support, str): raise Exception(dashboard_uid_support) if isinstance(datasource_uid_support, str): raise Exception(datasource_uid_support) - paging_support = paging_feature_check(grafana_url, http_get_headers, verify_ssl, client_cert, debug) + paging_support = paging_feature_check( + grafana_url, http_get_headers, verify_ssl, client_cert, debug) if isinstance(paging_support, str): raise Exception(paging_support) + is_contact_point_available = contact_point_check( + grafana_url, http_get_headers, verify_ssl, client_cert, debug) + print_horizontal_line() if status == 200: print("[Pre-Check] Server status is 'OK' !!") @@ -38,4 +45,4 @@ def main(settings): print("[Pre-Check] Server status is NOT OK !!: {0}".format(json_resp)) print_horizontal_line() - return (status, json_resp, dashboard_uid_support, datasource_uid_support, paging_support) + return (status, json_resp, dashboard_uid_support, datasource_uid_support, paging_support, is_contact_point_available) diff --git a/grafana_backup/archive.py b/grafana_backup/archive.py index 1db698a..1744e76 100755 --- a/grafana_backup/archive.py +++ b/grafana_backup/archive.py @@ -1,5 +1,7 @@ from glob import glob -import os, tarfile, shutil +import os +import tarfile +import shutil def main(args, settings): @@ -10,7 +12,7 @@ def main(args, settings): backup_files = list() for folder_name in ['folders', 'datasources', 'dashboards', 'alert_channels', 'organizations', 'users', 'snapshots', - 'dashboard_versions', 'annotations', 'library-elements', 'teams', 'team_members', 'alert_rules']: + 'dashboard_versions', 'annotations', 'library-elements', 'teams', 'team_members', 'alert_rules', 'contact_points', 'notification_policies']: backup_path = '{0}/{1}/{2}'.format(backup_dir, folder_name, timestamp) for file_path in glob(backup_path): diff --git a/grafana_backup/constants.py b/grafana_backup/constants.py index 6f1ab98..5ff8373 100644 --- a/grafana_backup/constants.py +++ b/grafana_backup/constants.py @@ -7,5 +7,5 @@ homedir = os.environ["HOME"] PKG_NAME = "grafana-backup" -PKG_VERSION = "1.3.3" +PKG_VERSION = "1.4.0" JSON_CONFIG_PATH = "{0}/.grafana-backup.json".format(homedir) diff --git a/grafana_backup/create_contact_point.py b/grafana_backup/create_contact_point.py new file mode 100644 index 0000000..d0a184a --- /dev/null +++ b/grafana_backup/create_contact_point.py @@ -0,0 +1,33 @@ +import json +from grafana_backup.dashboardApi import create_contact_point, get_grafana_version +from packaging import version + + +def main(args, settings, file_path): + grafana_url = settings.get('GRAFANA_URL') + http_post_headers = settings.get('HTTP_POST_HEADERS') + verify_ssl = settings.get('VERIFY_SSL') + client_cert = settings.get('CLIENT_CERT') + debug = settings.get('DEBUG') + + try: + grafana_version = get_grafana_version(grafana_url, verify_ssl) + except KeyError as error: + if not grafana_version: + raise Exception("Grafana version is not set.") from error + + minimum_version = version.parse('9.4.0') + + if minimum_version <= grafana_version: + with open(file_path, 'r') as f: + data = f.read() + + contact_points = json.loads(data) + for cp in contact_points: + result = create_contact_point(json.dumps( + cp), grafana_url, http_post_headers, verify_ssl, client_cert, debug) + print("create contact_point: {0}, status: {1}, msg: {2}".format( + cp['name'], result[0], result[1])) + else: + print("Unable to create contact points, requires Grafana version {0} or above. Current version is {1}".format( + minimum_version, grafana_version)) diff --git a/grafana_backup/dashboardApi.py b/grafana_backup/dashboardApi.py index 58d7d8d..82b6177 100755 --- a/grafana_backup/dashboardApi.py +++ b/grafana_backup/dashboardApi.py @@ -21,7 +21,8 @@ def auth_check(grafana_url, http_get_headers, verify_ssl, client_cert, debug): def uid_feature_check(grafana_url, http_get_headers, verify_ssl, client_cert, debug): # Get first dashboard on first page print("\n[Pre-Check] grafana uid feature check: calling 'search_dashboard'") - (status, content) = search_dashboard(1, 1, grafana_url, http_get_headers, verify_ssl, client_cert, debug) + (status, content) = search_dashboard(1, 1, grafana_url, + http_get_headers, verify_ssl, client_cert, debug) if status == 200 and len(content): if 'uid' in content[0]: dashboard_uid_support = True @@ -29,13 +30,15 @@ def uid_feature_check(grafana_url, http_get_headers, verify_ssl, client_cert, de dashboard_uid_support = False else: if len(content): - dashboard_uid_support = "get dashboards failed, status: {0}, msg: {1}".format(status, content) + dashboard_uid_support = "get dashboards failed, status: {0}, msg: {1}".format( + status, content) else: # No dashboards exist, disable uid feature dashboard_uid_support = False # Get first datasource print("\n[Pre-Check] grafana uid feature check: calling 'search_datasource'") - (status, content) = search_datasource(grafana_url, http_get_headers, verify_ssl, client_cert, debug) + (status, content) = search_datasource(grafana_url, + http_get_headers, verify_ssl, client_cert, debug) if status == 200 and len(content): if 'uid' in content[0]: datasource_uid_support = True @@ -43,7 +46,8 @@ def uid_feature_check(grafana_url, http_get_headers, verify_ssl, client_cert, de datasource_uid_support = False else: if len(content): - datasource_uid_support = "get datasources failed, status: {0}, msg: {1}".format(status, content) + datasource_uid_support = "get datasources failed, status: {0}, msg: {1}".format( + status, content) else: # No datasources exist, disable uid feature datasource_uid_support = False @@ -55,14 +59,19 @@ def paging_feature_check(grafana_url, http_get_headers, verify_ssl, client_cert, print("\n[Pre-Check] grafana paging_feature_check: calling 'search_dashboard'") def get_first_dashboard_by_page(page): - (status, content) = search_dashboard(page, 1, grafana_url, http_get_headers, verify_ssl, client_cert, debug) + (status, content) = search_dashboard(page, 1, grafana_url, + http_get_headers, verify_ssl, client_cert, debug) if status == 200 and len(content): if sys.version_info[0] > 2: - content[0] = {k: to_python2_and_3_compatible_string(v) for k,v in content[0].items()} - dashboard_values = sorted(content[0].items(), key=lambda kv: str(kv[1])) + content[0] = {k: to_python2_and_3_compatible_string( + v) for k, v in content[0].items()} + dashboard_values = sorted( + content[0].items(), key=lambda kv: str(kv[1])) else: - content[0] = {k: to_python2_and_3_compatible_string(unicode(v)) for k,v in content[0].iteritems()} - dashboard_values = sorted(content[0].iteritems(), key=lambda kv: str(kv[1])) + content[0] = {k: to_python2_and_3_compatible_string( + unicode(v)) for k, v in content[0].iteritems()} + dashboard_values = sorted( + content[0].iteritems(), key=lambda kv: str(kv[1])) return True, dashboard_values else: if len(content): @@ -93,8 +102,19 @@ def get_first_dashboard_by_page(page): return dashboard_one_values != dashboard_two_values +def contact_point_check(grafana_url, http_get_headers, verify_ssl, client_cert, debug): + print("\n[Pre-Check] grafana contact_point api check") + (status, content) = search_contact_points( + grafana_url, http_get_headers, verify_ssl, client_cert, debug) + if status == 200: + return True + else: + return False + + def search_dashboard(page, limit, grafana_url, http_get_headers, verify_ssl, client_cert, debug): - url = '{0}/api/search/?type=dash-db&limit={1}&page={2}'.format(grafana_url, limit, page) + url = '{0}/api/search/?type=dash-db&limit={1}&page={2}'.format( + grafana_url, limit, page) print("search dashboard in grafana: {0}".format(url)) return send_grafana_get(url, http_get_headers, verify_ssl, client_cert, debug) @@ -102,7 +122,8 @@ def search_dashboard(page, limit, grafana_url, http_get_headers, verify_ssl, cli def get_dashboard(board_uri, grafana_url, http_get_headers, verify_ssl, client_cert, debug): url = '{0}/api/dashboards/{1}'.format(grafana_url, board_uri) print("query dashboard uri: {0}".format(url)) - (status_code, content) = send_grafana_get(url, http_get_headers, verify_ssl, client_cert, debug) + (status_code, content) = send_grafana_get( + url, http_get_headers, verify_ssl, client_cert, debug) return (status_code, content) @@ -119,7 +140,7 @@ def create_library_element(library_element, grafana_url, http_post_headers, veri def delete_library_element(id_, grafana_url, http_get_headers, verify_ssl, client_cert, debug): return send_grafana_delete('{0}/api/library-elements/{1}'.format(grafana_url, id_), http_get_headers, - verify_ssl, client_cert) + verify_ssl, client_cert) def search_teams(grafana_url, http_get_headers, verify_ssl, client_cert, debug): @@ -135,7 +156,7 @@ def create_team(team, grafana_url, http_post_headers, verify_ssl, client_cert, d def delete_team(id_, grafana_url, http_get_headers, verify_ssl, client_cert, debug): return send_grafana_delete('{0}/api/teams/{1}'.format(grafana_url, id_), http_get_headers, - verify_ssl, client_cert) + verify_ssl, client_cert) def search_team_members(team_id, grafana_url, http_get_headers, verify_ssl, client_cert, debug): @@ -151,15 +172,17 @@ def create_team_member(user, team_id, grafana_url, http_post_headers, verify_ssl def delete_team_member(user_id, team_id, grafana_url, http_get_headers, verify_ssl, client_cert, debug): return send_grafana_delete('{0}/api/teams/{1}/members/{2}'.format(grafana_url, team_id, user_id), http_get_headers, - verify_ssl, client_cert) + verify_ssl, client_cert) def search_annotations(grafana_url, ts_from, ts_to, http_get_headers, verify_ssl, client_cert, debug): # there are two types of annotations # annotation: are user created, custom ones and can be managed via the api # alert: are created by Grafana itself, can NOT be managed by the api - url = '{0}/api/annotations?type=annotation&limit=5000&from={1}&to={2}'.format(grafana_url, ts_from, ts_to) - (status_code, content) = send_grafana_get(url, http_get_headers, verify_ssl, client_cert, debug) + url = '{0}/api/annotations?type=annotation&limit=5000&from={1}&to={2}'.format( + grafana_url, ts_from, ts_to) + (status_code, content) = send_grafana_get( + url, http_get_headers, verify_ssl, client_cert, debug) return (status_code, content) @@ -222,25 +245,27 @@ def delete_alert_channel_by_id(id_, grafana_url, http_post_headers, verify_ssl, def search_alerts(grafana_url, http_get_headers, verify_ssl, client_cert, debug): url = '{0}/api/alerts'.format(grafana_url) - (status_code, content) = send_grafana_get(url, http_get_headers, verify_ssl, client_cert, debug) + (status_code, content) = send_grafana_get( + url, http_get_headers, verify_ssl, client_cert, debug) return (status_code, content) def pause_alert(id_, grafana_url, http_post_headers, verify_ssl, client_cert, debug): url = '{0}/api/alerts/{1}/pause'.format(grafana_url, id_) payload = '{ "paused": true }' - (status_code, content) = send_grafana_post(url, payload, http_post_headers, verify_ssl, client_cert, debug) + (status_code, content) = send_grafana_post( + url, payload, http_post_headers, verify_ssl, client_cert, debug) return (status_code, content) def unpause_alert(id_, grafana_url, http_post_headers, verify_ssl, client_cert, debug): url = '{0}/api/alerts/{1}/pause'.format(grafana_url, id_) payload = '{ "paused": false }' - (status_code, content) = send_grafana_post(url, payload, http_post_headers, verify_ssl, client_cert, debug) + (status_code, content) = send_grafana_post( + url, payload, http_post_headers, verify_ssl, client_cert, debug) return (status_code, content) - def delete_folder(uid, grafana_url, http_post_headers, verify_ssl, client_cert, debug): return send_grafana_delete('{0}/api/folders/{1}'.format(grafana_url, uid), http_post_headers, verify_ssl, client_cert, debug) @@ -278,7 +303,8 @@ def search_snapshot(grafana_url, http_get_headers, verify_ssl, client_cert, debu def get_snapshot(key, grafana_url, http_get_headers, verify_ssl, client_cert, debug): url = '{0}/api/snapshots/{1}'.format(grafana_url, key) - (status_code, content) = send_grafana_get(url, http_get_headers, verify_ssl, client_cert, debug) + (status_code, content) = send_grafana_get( + url, http_get_headers, verify_ssl, client_cert, debug) return (status_code, content) @@ -333,7 +359,8 @@ def get_folder_id(dashboard, grafana_url, http_post_headers, verify_ssl, client_ try: folder_uid = dashboard['meta']['folderUid'] except (KeyError): - matches = re.search('dashboards\/f\/(.*)\/.*', dashboard['meta']['folderUrl']) + matches = re.search('dashboards\/f\/(.*)\/.*', + dashboard['meta']['folderUrl']) if matches is not None: folder_uid = matches.group(1) else: @@ -341,7 +368,8 @@ def get_folder_id(dashboard, grafana_url, http_post_headers, verify_ssl, client_ if (folder_uid != ""): print("debug: quering with uid {}".format(folder_uid)) - response = get_folder(folder_uid, grafana_url, http_post_headers, verify_ssl, client_cert, debug) + response = get_folder(folder_uid, grafana_url, + http_post_headers, verify_ssl, client_cert, debug) if isinstance(response[1], dict): folder_data = response[1] else: @@ -363,14 +391,16 @@ def create_folder(payload, grafana_url, http_post_headers, verify_ssl, client_ce def get_dashboard_versions(dashboard_id, grafana_url, http_get_headers, verify_ssl, client_cert, debug): (status_code, content) = send_grafana_get('{0}/api/dashboards/id/{1}/versions'.format(grafana_url, dashboard_id), http_get_headers, verify_ssl, client_cert, debug) - print("query dashboard versions: {0}, status: {1}".format(dashboard_id, status_code)) + print("query dashboard versions: {0}, status: {1}".format( + dashboard_id, status_code)) return (status_code, content) def get_version(dashboard_id, version_number, grafana_url, http_get_headers, verify_ssl, client_cert, debug): (status_code, content) = send_grafana_get('{0}/api/dashboards/id/{1}/versions/{2}'.format(grafana_url, dashboard_id, version_number), http_get_headers, verify_ssl, client_cert, debug) - print("query dashboard {0} version {1}, status: {2}".format(dashboard_id, version_number, status_code)) + print("query dashboard {0} version {1}, status: {2}".format( + dashboard_id, version_number, status_code)) return (status_code, content) @@ -406,7 +436,8 @@ def get_users(grafana_url, http_get_headers, verify_ssl, client_cert, debug): def set_user_role(user_id, role, grafana_url, http_post_headers, verify_ssl, client_cert, debug): json_payload = json.dumps({'role': role}) url = '{0}/api/org/users/{1}'.format(grafana_url, user_id) - r = requests.patch(url, headers=http_post_headers, data=json_payload, verify=verify_ssl, cert=client_cert) + r = requests.patch(url, headers=http_post_headers, + data=json_payload, verify=verify_ssl, cert=client_cert) return (r.status_code, r.json()) @@ -434,8 +465,26 @@ def add_user_to_org(org_id, payload, grafana_url, http_post_headers, verify_ssl, return send_grafana_post('{0}/api/orgs/{1}/users'.format(grafana_url, org_id), payload, http_post_headers, verify_ssl, client_cert, debug) + +def search_contact_points(grafana_url, http_get_headers, verify_ssl, client_cert, debug): + return send_grafana_get('{0}/api/v1/provisioning/contact-points'.format(grafana_url), http_get_headers, verify_ssl, client_cert, debug) + + +def create_contact_point(json_palyload, grafana_url, http_post_headers, verify_ssl, client_cert, debug): + return send_grafana_post('{0}/api/v1/provisioning/contact-points'.format(grafana_url), json_palyload, http_post_headers, verify_ssl, client_cert, debug) + + +def search_notification_policies(grafana_url, http_get_headers, verify_ssl, client_cert, debug): + return send_grafana_get('{0}/api/v1/provisioning/policies'.format(grafana_url), http_get_headers, verify_ssl, client_cert, debug) + + +def update_notification_policy(json_palyload, grafana_url, http_post_headers, verify_ssl, client_cert, debug): + return send_grafana_put('{0}/api/v1/provisioning/policies'.format(grafana_url), json_palyload, http_post_headers, verify_ssl, client_cert, debug) + + def get_grafana_version(grafana_url, verify_ssl, http_get_headers): - r = requests.get('{0}/api/health'.format(grafana_url), verify=verify_ssl, headers=http_get_headers) + r = requests.get('{0}/api/health'.format(grafana_url), + verify=verify_ssl, headers=http_get_headers) if r.status_code == 200: if 'version' in r.json().keys(): version_str = r.json()['version'] @@ -446,23 +495,29 @@ def get_grafana_version(grafana_url, verify_ssl, http_get_headers): if match: version_number = match.group(1) else: - raise Exception("version key found but string value could not be parsed, returned respone: {0}".format(r.json)) + raise Exception( + "version key found but string value could not be parsed, returned respone: {0}".format(r.json)) return version.parse(version_number) else: - raise KeyError("Unable to get version, returned respone: {0}".format(r.json)) + raise KeyError( + "Unable to get version, returned respone: {0}".format(r.json)) else: - raise Exception("Unable to get version, returned response: {0}".format(r.status_code)) + raise Exception( + "Unable to get version, returned response: {0}".format(r.status_code)) + def send_grafana_get(url, http_get_headers, verify_ssl, client_cert, debug): - r = requests.get(url, headers=http_get_headers, verify=verify_ssl, cert=client_cert) + r = requests.get(url, headers=http_get_headers, + verify=verify_ssl, cert=client_cert) if debug: log_response(r) return (r.status_code, r.json()) def send_grafana_post(url, json_payload, http_post_headers, verify_ssl=False, client_cert=None, debug=True): - r = requests.post(url, headers=http_post_headers, data=json_payload, verify=verify_ssl, cert=client_cert) + r = requests.post(url, headers=http_post_headers, + data=json_payload, verify=verify_ssl, cert=client_cert) if debug: log_response(r) try: @@ -472,7 +527,8 @@ def send_grafana_post(url, json_payload, http_post_headers, verify_ssl=False, cl def send_grafana_put(url, json_payload, http_post_headers, verify_ssl=False, client_cert=None, debug=True): - r = requests.put(url, headers=http_post_headers, data=json_payload, verify=verify_ssl, cert=client_cert) + r = requests.put(url, headers=http_post_headers, + data=json_payload, verify=verify_ssl, cert=client_cert) if debug: log_response(r) return (r.status_code, r.json()) diff --git a/grafana_backup/delete.py b/grafana_backup/delete.py index f6104e3..da58760 100755 --- a/grafana_backup/delete.py +++ b/grafana_backup/delete.py @@ -24,7 +24,8 @@ def main(args, settings): 'library-elements': delete_library_elements, 'team-members': delete_team_members} - (status, json_resp, dashboard_uid_support, datasource_uid_support, paging_support) = api_checks(settings) + (status, json_resp, dashboard_uid_support, + datasource_uid_support, paging_support) = api_checks(settings) # Do not continue if API is unavailable or token is not valid if not status == 200: diff --git a/grafana_backup/restore.py b/grafana_backup/restore.py index 01c7c07..cc805e4 100755 --- a/grafana_backup/restore.py +++ b/grafana_backup/restore.py @@ -12,6 +12,8 @@ from grafana_backup.create_team import main as create_team from grafana_backup.create_team_member import main as create_team_member from grafana_backup.create_library_element import main as create_library_element +from grafana_backup.create_contact_point import main as create_contact_point +from grafana_backup.update_notification_policy import main as update_notification_policy from grafana_backup.s3_download import main as s3_download from grafana_backup.azure_storage_download import main as azure_storage_download from grafana_backup.gcs_download import main as gcs_download @@ -39,7 +41,9 @@ def open_compressed_backup(compressed_backup): azure_storage_container_name = settings.get('AZURE_STORAGE_CONTAINER_NAME') gcs_bucket_name = settings.get('GCS_BUCKET_NAME') - (status, json_resp, dashboard_uid_support, datasource_uid_support, paging_support) = api_checks(settings) + (status, json_resp, dashboard_uid_support, datasource_uid_support, + paging_support, contact_point_support) = api_checks(settings) + settings.update({'CONTACT_POINT_SUPPORT': contact_point_support}) # Do not continue if API is unavailable or token is not valid if not status == 200: @@ -77,9 +81,11 @@ def open_compressed_backup(compressed_backup): # Shell game magic warning: restore_function keys require the 's' # to be removed in order to match file extension names... restore_functions = collections.OrderedDict() - restore_functions['folder'] = create_folder # Folders must be restored before Library-Elements + # Folders must be restored before Library-Elements + restore_functions['folder'] = create_folder restore_functions['datasource'] = create_datasource - restore_functions['library_element'] = create_library_element # Library-Elements must be restored before dashboards + # Library-Elements must be restored before dashboards + restore_functions['library_element'] = create_library_element restore_functions['dashboard'] = create_dashboard restore_functions['alert_channel'] = create_alert_channel restore_functions['organization'] = create_org @@ -90,6 +96,9 @@ def open_compressed_backup(compressed_backup): restore_functions['team_member'] = create_team_member restore_functions['folder_permission'] = update_folder_permissions restore_functions['alert_rule'] = create_alert_rule + restore_functions['contact_point'] = create_contact_point + # There are some issues of notification policy restore api, it will lock the notification policy page and cannot be edited. + # restore_functions['notification_policys'] = update_notification_policy if sys.version_info >= (3,): with tempfile.TemporaryDirectory() as tmpdir: @@ -108,7 +117,7 @@ def open_compressed_backup(compressed_backup): def restore_components(args, settings, restore_functions, tmpdir): - arg_components = args.get('--components', False) + arg_components = args.get('--components', []) if arg_components: arg_components_list = arg_components.replace("-", "_").split(',') diff --git a/grafana_backup/save.py b/grafana_backup/save.py index f7e5b2a..70eb402 100755 --- a/grafana_backup/save.py +++ b/grafana_backup/save.py @@ -7,6 +7,8 @@ from grafana_backup.save_snapshots import main as save_snapshots from grafana_backup.save_dashboard_versions import main as save_dashboard_versions from grafana_backup.save_annotations import main as save_annotations +from grafana_backup.save_contact_points import main as save_contact_points +from grafana_backup.save_notification_policies import main as save_notification_policies from grafana_backup.archive import main as archive from grafana_backup.s3_upload import main as s3_upload from grafana_backup.influx import main as influx @@ -17,6 +19,7 @@ from grafana_backup.save_team_members import main as save_team_members from grafana_backup.azure_storage_upload import main as azure_storage_upload from grafana_backup.gcs_upload import main as gcs_upload +from grafana_backup.commons import print_horizontal_line import sys @@ -31,24 +34,33 @@ def main(args, settings): 'organizations': save_orgs, 'users': save_users, 'snapshots': save_snapshots, - 'versions': save_dashboard_versions, # left for backwards compatibility + 'versions': save_dashboard_versions, # left for backwards compatibility 'dashboard-versions': save_dashboard_versions, 'annotations': save_annotations, 'library-elements': save_library_elements, 'teams': save_teams, 'team-members': save_team_members, - 'alert-rules': save_alert_rules} + 'alert-rules': save_alert_rules, + 'contact-points': save_contact_points, + 'notification-policy': save_notification_policies, + } - (status, json_resp, dashboard_uid_support, datasource_uid_support, paging_support) = api_checks(settings) - - # Do not continue if API is unavailable or token is not valid - if not status == 200: - print("server status is not ok: {0}".format(json_resp)) - sys.exit(1) + (status, + json_resp, + dashboard_uid_support, + datasource_uid_support, + paging_support, + contact_point_support) = api_checks(settings) settings.update({'DASHBOARD_UID_SUPPORT': dashboard_uid_support}) settings.update({'DATASOURCE_UID_SUPPORT': datasource_uid_support}) settings.update({'PAGING_SUPPORT': paging_support}) + settings.update({'CONTACT_POINT_SUPPORT': contact_point_support}) + + # Do not continue if API is unavailable or token is not valid + if not status == 200: + print("grafana server status is not ok: {0}".format(json_resp)) + sys.exit(1) if arg_components: arg_components_list = arg_components.replace("_", "-").split(',') diff --git a/grafana_backup/save_contact_points.py b/grafana_backup/save_contact_points.py new file mode 100644 index 0000000..bd3ded6 --- /dev/null +++ b/grafana_backup/save_contact_points.py @@ -0,0 +1,67 @@ +import os +from grafana_backup.dashboardApi import search_contact_points, get_grafana_version +from grafana_backup.commons import to_python2_and_3_compatible_string, print_horizontal_line, save_json +from packaging import version + + +def main(args, settings): + backup_dir = settings.get('BACKUP_DIR') + timestamp = settings.get('TIMESTAMP') + grafana_url = settings.get('GRAFANA_URL') + http_get_headers = settings.get('HTTP_GET_HEADERS') + verify_ssl = settings.get('VERIFY_SSL') + client_cert = settings.get('CLIENT_CERT') + debug = settings.get('DEBUG') + pretty_print = settings.get('PRETTY_PRINT') + folder_path = '{0}/contact_points/{1}'.format(backup_dir, timestamp) + log_file = 'contact_points_{0}.txt'.format(timestamp) + grafana_version_string = settings.get('GRAFANA_VERSION') + + if grafana_version_string: + grafana_version = version.parse(grafana_version_string) + + try: + grafana_version = get_grafana_version(grafana_url, verify_ssl) + except KeyError as error: + if not grafana_version: + raise Exception("Grafana version is not set.") from error + + minimum_version = version.parse('9.0.0') + if minimum_version <= grafana_version: + if not os.path.exists(folder_path): + os.makedirs(folder_path) + + contact_points = get_all_contact_points_in_grafana( + grafana_url, http_get_headers, verify_ssl, client_cert, debug) + save_contact_points('contact_points', contact_points, + folder_path, pretty_print) + else: + print("Unable to save contact points, requires Grafana version {0} or above. Current version is {1}".format( + minimum_version, grafana_version)) + + if not os.path.exists(folder_path): + os.makedirs(folder_path) + + +def get_all_contact_points_in_grafana(grafana_url, http_get_headers, verify_ssl, client_cert, debug): + (status, content) = search_contact_points( + grafana_url, http_get_headers, verify_ssl, client_cert, debug) + if status == 200: + contact_points = content + print("There are {0} contact points: ".format(len(contact_points))) + for contact_point in contact_points: + print("name: {0}, type: {1}".format(to_python2_and_3_compatible_string( + contact_point['name']), to_python2_and_3_compatible_string(contact_point['type']))) + return contact_points + else: + print("query contact points failed, status: {0}, msg: {1}".format( + status, content)) + return [] + + +def save_contact_points(file_name, contact_points, folder_path, pretty_print): + file_path = save_json(file_name, contact_points, + folder_path, 'contact_point', pretty_print) + print_horizontal_line() + print("contact points are saved to {0}".format(file_path)) + print_horizontal_line() diff --git a/grafana_backup/save_notification_policies.py b/grafana_backup/save_notification_policies.py new file mode 100644 index 0000000..38ab281 --- /dev/null +++ b/grafana_backup/save_notification_policies.py @@ -0,0 +1,64 @@ +import os +from grafana_backup.dashboardApi import search_notification_policies, get_grafana_version +from grafana_backup.commons import to_python2_and_3_compatible_string, print_horizontal_line, save_json +from packaging import version + + +def main(args, settings): + backup_dir = settings.get('BACKUP_DIR') + timestamp = settings.get('TIMESTAMP') + grafana_url = settings.get('GRAFANA_URL') + http_get_headers = settings.get('HTTP_GET_HEADERS') + verify_ssl = settings.get('VERIFY_SSL') + client_cert = settings.get('CLIENT_CERT') + debug = settings.get('DEBUG') + pretty_print = settings.get('PRETTY_PRINT') + folder_path = '{0}/notification_policies/{1}'.format(backup_dir, timestamp) + log_file = 'notification_policies_{0}.txt'.format(timestamp) + grafana_version_string = settings.get('GRAFANA_VERSION') + + if grafana_version_string: + grafana_version = version.parse(grafana_version_string) + + try: + grafana_version = get_grafana_version(grafana_url, verify_ssl) + except KeyError as error: + if not grafana_version: + raise Exception("Grafana version is not set.") from error + + minimum_version = version.parse('9.0.0') + if minimum_version <= grafana_version: + if not os.path.exists(folder_path): + os.makedirs(folder_path) + + notification_policies = get_all_notification_policies_in_grafana( + grafana_url, http_get_headers, verify_ssl, client_cert, debug) + save_notification_policies( + 'notificatioin_policies', notification_policies, folder_path, pretty_print) + else: + print("Unable to save notification policies, requires Grafana version {0} or above. Current version is {1}".format( + minimum_version, grafana_version)) + + if not os.path.exists(folder_path): + os.makedirs(folder_path) + + +def get_all_notification_policies_in_grafana(grafana_url, http_get_headers, verify_ssl, client_cert, debug): + (status, content) = search_notification_policies( + grafana_url, http_get_headers, verify_ssl, client_cert, debug) + if status == 200: + notification_policies = content + print("Notification policies found") + return notification_policies + else: + print("query notification policies failed, status: {0}, msg: {1}".format( + status, content)) + return [] + + +def save_notification_policies(file_name, notification_policies, folder_path, pretty_print): + file_path = save_json(file_name, notification_policies, + folder_path, 'notification_policys', pretty_print) + print_horizontal_line() + print("notification policies are saved to {0}".format(file_path)) + print_horizontal_line() diff --git a/grafana_backup/update_notification_policy.py b/grafana_backup/update_notification_policy.py new file mode 100644 index 0000000..e2e7106 --- /dev/null +++ b/grafana_backup/update_notification_policy.py @@ -0,0 +1,32 @@ +import json +from grafana_backup.dashboardApi import update_notification_policy, get_grafana_version +from packaging import version + + +def main(args, settings, file_path): + grafana_url = settings.get('GRAFANA_URL') + http_post_headers = settings.get('HTTP_POST_HEADERS') + verify_ssl = settings.get('VERIFY_SSL') + client_cert = settings.get('CLIENT_CERT') + debug = settings.get('DEBUG') + + try: + grafana_version = get_grafana_version(grafana_url, verify_ssl) + except KeyError as error: + if not grafana_version: + raise Exception("Grafana version is not set.") from error + + minimum_version = version.parse('9.4.0') + + if minimum_version <= grafana_version: + with open(file_path, 'r') as f: + data = f.read() + + notification_policies = json.loads(data) + result = update_notification_policy(json.dumps( + notification_policies), grafana_url, http_post_headers, verify_ssl, client_cert, debug) + print("update notification_policy, status: {0}, msg: {1}".format( + result[0], result[1])) + else: + print("Unable to update notification policy, requires Grafana version {0} or above. Current version is {1}".format( + minimum_version, grafana_version))