Skip to content

Commit c638680

Browse files
authored
Merge pull request #282 from nikodemas/add_grafana_backup
Add grafana production copy cronjob
2 parents c5c5513 + 2f8fb56 commit c638680

File tree

6 files changed

+219
-2
lines changed

6 files changed

+219
-2
lines changed

grafana-backup/README.md grafana-backup/dashboard-exporter/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Then compresses and puts them to EOS folder `/eos/cms/store/group/offcomp_monit/
99

1010
```
1111
{
12-
"SECRET_KEY": "YOUR_SECRET_KEY"
12+
"filesystem_exporter_token": "YOUR_SECRET_KEY"
1313
}
1414
```
1515

grafana-backup/dashboard-exporter.py grafana-backup/dashboard-exporter/dashboard-exporter.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def get_grafana_auth(fname):
2525
print(f"File {fname} does not exist")
2626
sys.exit(1)
2727
with open(fname, "r") as keyFile:
28-
secret_key = json.load(keyFile).get("SECRET_KEY")
28+
secret_key = json.load(keyFile).get("filesystem_exporter_token")
2929
headers = {"Authorization": f"Bearer {secret_key}"}
3030
return headers
3131

File renamed without changes.
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
## Production Copy
3+
This script copies Grafana dashboards from the folder "Production" to "Production Copy" using the Grafana API.
4+
5+
## Requirements
6+
- Create a file for your Grafana authentication token and name it `keys.json`
7+
8+
```
9+
{
10+
"production_copy_token": "YOUR_SECRET_KEY"
11+
}
12+
```
13+
14+
- Get `amtool` executable
15+
```
16+
curl -ksLO https://github.com/prometheus/alertmanager/releases/download/v0.27.0/alertmanager-0.27.0.linux-amd64.tar.gz && \
17+
tar xfz alertmanager-0.27.0.linux-amd64.tar.gz && \
18+
mv alertmanager-0.27.0.linux-amd64/amtool . && \
19+
rm -rf alertmanager-0.27.0.linux-amd64*
20+
```
21+
22+
## How to use
23+
24+
- Run the file:
25+
```sh
26+
python3 dashboard-copy.py --url https://grafana-url.com --token keys.json
27+
```
28+
- Set the file executable:
29+
```sh
30+
chmod +x ./dashboard-copy.py
31+
```
32+
- Set crontab for preferred timeframe
33+
- See `run.sh` file
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
"""
4+
This script copies Grafana dashboards from one folder to another.
5+
"""
6+
import datetime
7+
import json
8+
import logging
9+
import sys
10+
11+
import click
12+
import requests
13+
14+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
15+
16+
17+
def get_grafana_auth(fname):
18+
"""Load Grafana authentication token from a file."""
19+
with open(fname, "r") as token_file:
20+
token = json.load(token_file)["production_copy_token"]
21+
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
22+
23+
24+
def get_folder_id(base_url, headers, folder_name):
25+
"""Fetch the folder ID for a given folder name."""
26+
response = requests.get(f"{base_url}/api/folders", headers=headers)
27+
response.raise_for_status()
28+
for folder in response.json():
29+
if folder["title"] == folder_name:
30+
return folder["id"]
31+
return None
32+
33+
34+
def get_folder_uid(base_url, headers, folder_name):
35+
"""Fetch the folder UID for a given folder name."""
36+
response = requests.get(f"{base_url}/api/folders", headers=headers)
37+
response.raise_for_status()
38+
for folder in response.json():
39+
if folder["title"] == folder_name:
40+
return folder["uid"]
41+
return None
42+
43+
44+
def get_folder_permissions(base_url, headers, folder_uid):
45+
"""Fetch the permissions of a folder."""
46+
response = requests.get(f"{base_url}/api/folders/{folder_uid}/permissions", headers=headers)
47+
response.raise_for_status()
48+
return response.json()
49+
50+
51+
def update_folder_permissions(base_url, headers, folder_uid, permissions):
52+
"""Set permissions for a folder."""
53+
payload = {"items": permissions}
54+
response = requests.post(f"{base_url}/api/folders/{folder_uid}/permissions", headers=headers, json=payload)
55+
response.raise_for_status()
56+
logging.info(f"Permissions for folder UID '{folder_uid}' have been updated.")
57+
58+
59+
def delete_folder(base_url, headers, folder_uid):
60+
"""Delete a folder by its UID."""
61+
response = requests.delete(f"{base_url}/api/folders/{folder_uid}", headers=headers)
62+
response.raise_for_status()
63+
logging.info(f"Deleted folder with UID {folder_uid}.")
64+
65+
66+
def create_folder(base_url, headers, folder_name):
67+
"""Create a folder. If it exists, delete and recreate it while preserving permissions."""
68+
folder_uid = get_folder_uid(base_url, headers, folder_name)
69+
preserved_permissions = []
70+
71+
if folder_uid:
72+
logging.info(f"Folder '{folder_name}' exists. Fetching permissions and overwriting...")
73+
preserved_permissions = get_folder_permissions(base_url, headers, folder_uid)
74+
delete_folder(base_url, headers, folder_uid)
75+
76+
response = requests.post(f"{base_url}/api/folders", headers=headers, json={"title": folder_name})
77+
response.raise_for_status()
78+
folder = response.json()
79+
logging.info(f"Created folder '{folder_name}'.")
80+
81+
if preserved_permissions:
82+
update_folder_permissions(base_url, headers, folder["uid"], preserved_permissions)
83+
84+
return folder["id"]
85+
86+
87+
def get_dashboards_in_folder(base_url, headers, folder_id):
88+
"""Fetch all dashboards in a folder."""
89+
response = requests.get(f"{base_url}/api/search?folderIds={folder_id}&type=dash-db", headers=headers)
90+
response.raise_for_status()
91+
return response.json()
92+
93+
94+
def copy_dashboard(base_url, headers, dashboard, target_folder_id):
95+
"""Copy a dashboard to a new folder."""
96+
dashboard_uid = dashboard["uid"]
97+
response = requests.get(f"{base_url}/api/dashboards/uid/{dashboard_uid}", headers=headers)
98+
response.raise_for_status()
99+
dashboard_data = response.json()
100+
dashboard_data["dashboard"]["id"] = None
101+
dashboard_data["dashboard"]["uid"] = None
102+
dashboard_data["folderId"] = target_folder_id
103+
dashboard_data["message"] = f"Copied on {datetime.datetime.now()}"
104+
response = requests.post(f"{base_url}/api/dashboards/db", headers=headers, json=dashboard_data)
105+
response.raise_for_status()
106+
logging.info(f"Copied dashboard '{dashboard['title']}' to folder ID {target_folder_id}.")
107+
108+
109+
@click.command()
110+
@click.option("--url", required=True, help="Base URL of the Grafana instance")
111+
@click.option("--token", "fname", required=True, help="API or Service Account token for authentication")
112+
def main(url, fname):
113+
headers = get_grafana_auth(fname)
114+
source_folder_name = "Production"
115+
target_folder_name = "Production Copy"
116+
117+
source_folder_id = get_folder_id(url, headers, source_folder_name)
118+
if not source_folder_id:
119+
logging.error(f"Source folder '{source_folder_name}' does not exist.")
120+
return
121+
122+
target_folder_id = create_folder(url, headers, target_folder_name)
123+
dashboards = get_dashboards_in_folder(url, headers, source_folder_id)
124+
logging.info(f"Found {len(dashboards)} dashboards in '{source_folder_name}'.")
125+
126+
for dashboard in dashboards:
127+
copy_dashboard(url, headers, dashboard, target_folder_id)
128+
129+
130+
if __name__ == "__main__":
131+
try:
132+
main()
133+
except Exception as e:
134+
logging.error(f"An unexpected error occurred: {e}")
135+
sys.exit(1)

grafana-backup/production-copy/run.sh

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/bin/bash -l
2+
3+
set -e
4+
5+
# This script copies Grafana dashboards from the folder "Production" to "Production Copy".
6+
##H Usage: run.sh GRAFANA_URL API_TOKEN
7+
##H Example:
8+
##H run.sh https://grafana-url.com your-api-token
9+
10+
if [ "$1" == "-h" ] || [ "$1" == "-help" ] || [ "$1" == "--help" ] || [ "$1" == "help" ] || [ "$1" == "" ]; then
11+
grep "^##H" <"$0" | sed -e "s,##H,,g"
12+
exit 1
13+
fi
14+
15+
# Arguments
16+
GRAFANA_URL="$1"
17+
API_TOKEN="$2"
18+
19+
20+
21+
trap onExit EXIT
22+
23+
function onExit() {
24+
local status=$?
25+
if [ $status -ne 0 ]; then
26+
local msg="Grafana dashboard copy cron failure. Please see Kubernetes 'cron' cluster logs."
27+
if [ -x ./amtool ]; then
28+
expire=$(date -d '+1 hour' --rfc-3339=ns | tr ' ' 'T')
29+
local urls="http://cms-monitoring.cern.ch:30093 http://cms-monitoring-ha1.cern.ch:30093 http://cms-monitoring-ha2.cern.ch:30093"
30+
for url in $urls; do
31+
./amtool alert add grafana_dashboard_copy_failure \
32+
alertname=grafana_dashboard_copy_failure severity=monitoring tag=cronjob alert=amtool \
33+
--end="$expire" \
34+
--annotation=summary="$msg" \
35+
--annotation=date="$(date)" \
36+
--annotation=hostname="$(hostname)" \
37+
--annotation=status="$status" \
38+
--alertmanager.url="$url"
39+
done
40+
else
41+
echo "$msg" | mail -s "Cron alert grafana_dashboard_copy_failure" "$addr"
42+
fi
43+
fi
44+
}
45+
46+
cd "$(dirname "$0")" || exit
47+
48+
# Execute the Python script
49+
python3 dashboard-copy.py --url "$GRAFANA_URL" --token "$API_TOKEN"

0 commit comments

Comments
 (0)