#!/bin/bash ################################################ #### Grafana Backup & Restore Script #### #### Backup dashboards, datasources, alert #### #### rules, and folders via the HTTP API #### #### #### #### Author: Phil Connor #### #### Contact: contact@mylinux.work #### #### Version: 1.0.0.20260309 #### ################################################ set -o pipefail SCRIPT_NAME=$(basename "$0") readonly SCRIPT_NAME # Default configuration readonly DEFAULT_BACKUP_DIR="/var/backups/grafana" readonly DEFAULT_RETENTION_COUNT=7 readonly DEFAULT_CURL_TIMEOUT=30 # Configuration variables (can be overridden by environment) GRAFANA_URL=${GRAFANA_URL:-} GRAFANA_TOKEN=${GRAFANA_TOKEN:-} BACKUP_DIR=${BACKUP_DIR:-$DEFAULT_BACKUP_DIR} RETENTION_COUNT=${RETENTION_COUNT:-$DEFAULT_RETENTION_COUNT} # Runtime RUN_MODE="backup" RESTORE_DIR="" handle_error() { local exit_code=$1 local line_number=$2 echo "Error: $SCRIPT_NAME failed at line $line_number with exit code $exit_code" >&2 exit "$exit_code" } trap 'handle_error $? $LINENO' ERR show_help() { cat << EOF Usage: $SCRIPT_NAME [OPTIONS] Backup and restore Grafana dashboards, datasources, alert rules, and folders via the HTTP API. Creates timestamped backup directories with automatic retention. OPTIONS: --backup Run a full backup (default) --restore DIR Restore from the specified backup directory --list List available backups --help, -h Show this help message ENVIRONMENT VARIABLES: GRAFANA_URL Grafana base URL (required, e.g. http://localhost:3000) GRAFANA_TOKEN Grafana API token with Admin permissions (required) BACKUP_DIR Root backup directory (default: $DEFAULT_BACKUP_DIR) RETENTION_COUNT Number of backups to retain (default: $DEFAULT_RETENTION_COUNT) EXAMPLES: GRAFANA_URL=http://localhost:3000 GRAFANA_TOKEN=glsa_xxxx $SCRIPT_NAME --backup GRAFANA_URL=http://localhost:3000 GRAFANA_TOKEN=glsa_xxxx $SCRIPT_NAME --restore /var/backups/grafana/20260309-143022 GRAFANA_URL=http://localhost:3000 GRAFANA_TOKEN=glsa_xxxx $SCRIPT_NAME --list EOF exit 0 } # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in --backup) RUN_MODE="backup"; shift ;; --restore) RUN_MODE="restore"; RESTORE_DIR="$2"; shift 2 ;; --list) RUN_MODE="list"; shift ;; --help|-h) show_help ;; *) echo "Unknown option: $1" >&2; show_help ;; esac done validate_config() { if [[ -z "$GRAFANA_URL" ]]; then echo "Error: GRAFANA_URL is required" >&2 exit 1 fi if [[ -z "$GRAFANA_TOKEN" ]]; then echo "Error: GRAFANA_TOKEN is required" >&2 exit 1 fi # Strip trailing slash GRAFANA_URL="${GRAFANA_URL%/}" # Check dependencies for cmd in curl jq; do if ! command -v "$cmd" &>/dev/null; then echo "Error: $cmd is required but not installed" >&2 exit 1 fi done } grafana_api() { local method="$1" local endpoint="$2" local data="${3:-}" local url="${GRAFANA_URL}${endpoint}" local args=( -sf --max-time "$DEFAULT_CURL_TIMEOUT" -H "Authorization: Bearer ${GRAFANA_TOKEN}" -H "Content-Type: application/json" -H "Accept: application/json" -X "$method" ) if [[ -n "$data" ]]; then args+=(-d "$data") fi curl "${args[@]}" "$url" } backup_dashboards() { local dest="$1/dashboards" mkdir -p "$dest" echo "Backing up dashboards..." local search_result search_result=$(grafana_api GET "/api/search?type=dash-db&limit=5000") || { echo " Error: Failed to search dashboards" >&2 return 1 } local count count=$(echo "$search_result" | jq 'length') echo " Found $count dashboards" echo "$search_result" | jq -r '.[].uid' | while IFS= read -r uid; do local dashboard dashboard=$(grafana_api GET "/api/dashboards/uid/$uid") || { echo " Warning: Failed to export dashboard $uid" >&2 continue } local title title=$(echo "$dashboard" | jq -r '.dashboard.title // "unknown"' | tr '/ ' '__') echo "$dashboard" | jq '.' > "$dest/${uid}_${title}.json" done echo " Dashboards saved to $dest" } backup_datasources() { local dest="$1/datasources" mkdir -p "$dest" echo "Backing up datasources..." local result result=$(grafana_api GET "/api/datasources") || { echo " Error: Failed to fetch datasources" >&2 return 1 } local count count=$(echo "$result" | jq 'length') echo " Found $count datasources" echo "$result" | jq -c '.[]' | while IFS= read -r ds; do local id name id=$(echo "$ds" | jq -r '.id') name=$(echo "$ds" | jq -r '.name' | tr '/ ' '__') echo "$ds" | jq '.' > "$dest/${id}_${name}.json" done echo " Datasources saved to $dest" } backup_alert_rules() { local dest="$1/alert_rules" mkdir -p "$dest" echo "Backing up alert rules..." local result result=$(grafana_api GET "/api/v1/provisioning/alert-rules") || { echo " Error: Failed to fetch alert rules" >&2 return 1 } local count count=$(echo "$result" | jq 'length') echo " Found $count alert rules" echo "$result" | jq -c '.[]' | while IFS= read -r rule; do local uid title uid=$(echo "$rule" | jq -r '.uid // .id // "unknown"') title=$(echo "$rule" | jq -r '.title // "unknown"' | tr '/ ' '__') echo "$rule" | jq '.' > "$dest/${uid}_${title}.json" done echo " Alert rules saved to $dest" } backup_folders() { local dest="$1/folders" mkdir -p "$dest" echo "Backing up folders..." local result result=$(grafana_api GET "/api/folders?limit=1000") || { echo " Error: Failed to fetch folders" >&2 return 1 } local count count=$(echo "$result" | jq 'length') echo " Found $count folders" echo "$result" | jq -c '.[]' | while IFS= read -r folder; do local uid title uid=$(echo "$folder" | jq -r '.uid') title=$(echo "$folder" | jq -r '.title' | tr '/ ' '__') echo "$folder" | jq '.' > "$dest/${uid}_${title}.json" done echo " Folders saved to $dest" } restore_dashboards() { local src="$1/dashboards" if [[ ! -d "$src" ]]; then echo " No dashboards directory found, skipping" >&2 return 0 fi echo "Restoring dashboards..." local count=0 for file in "$src"/*.json; do [[ -f "$file" ]] || continue local payload payload=$(jq '{dashboard: .dashboard, overwrite: true, folderId: (.meta.folderId // 0)}' "$file") || { echo " Warning: Failed to parse $file" >&2 continue } if grafana_api POST "/api/dashboards/db" "$payload" >/dev/null; then count=$((count + 1)) else local title title=$(basename "$file" .json) echo " Warning: Failed to restore dashboard $title" >&2 fi done echo " Restored $count dashboards" } restore_datasources() { local src="$1/datasources" if [[ ! -d "$src" ]]; then echo " No datasources directory found, skipping" >&2 return 0 fi echo "Restoring datasources..." local count=0 for file in "$src"/*.json; do [[ -f "$file" ]] || continue local payload payload=$(jq 'del(.id, .uid, .readOnly)' "$file") || { echo " Warning: Failed to parse $file" >&2 continue } if grafana_api POST "/api/datasources" "$payload" >/dev/null; then count=$((count + 1)) else local name name=$(basename "$file" .json) echo " Warning: Failed to restore datasource $name" >&2 fi done echo " Restored $count datasources" } prune_backups() { echo "Pruning old backups (retaining $RETENTION_COUNT)..." local backup_count backup_count=$(find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l) if [[ $backup_count -le $RETENTION_COUNT ]]; then echo " No pruning needed ($backup_count backups present)" return 0 fi local remove_count=$((backup_count - RETENTION_COUNT)) find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d | sort | head -n "$remove_count" | while IFS= read -r dir; do echo " Removing $(basename "$dir")" rm -rf "$dir" done echo " Pruned $remove_count old backups" } do_backup() { local timestamp timestamp=$(date +%Y%m%d-%H%M%S) local dest="$BACKUP_DIR/$timestamp" mkdir -p "$dest" echo "Starting backup to $dest" backup_dashboards "$dest" backup_datasources "$dest" backup_alert_rules "$dest" backup_folders "$dest" prune_backups echo "Backup complete: $dest" } do_restore() { if [[ -z "$RESTORE_DIR" ]]; then echo "Error: --restore requires a directory argument" >&2 exit 1 fi if [[ ! -d "$RESTORE_DIR" ]]; then echo "Error: Restore directory does not exist: $RESTORE_DIR" >&2 exit 1 fi echo "Restoring from $RESTORE_DIR" restore_dashboards "$RESTORE_DIR" restore_datasources "$RESTORE_DIR" echo "Restore complete" } do_list() { if [[ ! -d "$BACKUP_DIR" ]]; then echo "No backups found (directory does not exist: $BACKUP_DIR)" return 0 fi local count=0 for dir in "$BACKUP_DIR"/*/; do [[ -d "$dir" ]] || continue count=$((count + 1)) local name name=$(basename "$dir") local dashboards datasources alerts folders dashboards=$(find "$dir/dashboards" -name '*.json' 2>/dev/null | wc -l) datasources=$(find "$dir/datasources" -name '*.json' 2>/dev/null | wc -l) alerts=$(find "$dir/alert_rules" -name '*.json' 2>/dev/null | wc -l) folders=$(find "$dir/folders" -name '*.json' 2>/dev/null | wc -l) printf " %s dashboards:%-4s datasources:%-4s alerts:%-4s folders:%-4s\n" \ "$name" "$dashboards" "$datasources" "$alerts" "$folders" done if [[ $count -eq 0 ]]; then echo "No backups found in $BACKUP_DIR" else echo "$count backup(s) in $BACKUP_DIR" fi } main() { validate_config case "$RUN_MODE" in backup) do_backup ;; restore) do_restore ;; list) do_list ;; esac } main