.
This commit is contained in:
17
settings.env
Normal file
17
settings.env
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# ==========================================
|
||||||
|
# Portainer Stack Backup – Settings
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Portainer Verbindung
|
||||||
|
PORTAINER_URL="https://192.168.178.25:9443"
|
||||||
|
|
||||||
|
# Portainer API Key
|
||||||
|
API_KEY="ptr_QX6nhHj2A9wF/XD4wEQ6z/b+udOkIC3Ocao+bC1PP7M="
|
||||||
|
|
||||||
|
# Zielverzeichnis für Backups
|
||||||
|
OUT_DIR="/home/thorsten/Schreibtisch/Backup"
|
||||||
|
|
||||||
|
# Archiv erstellen?
|
||||||
|
# 1 = tar.gz erzeugen
|
||||||
|
# 0 = nur Ordner behalten
|
||||||
|
CREATE_ARCHIVE=1
|
||||||
164
shell_portainer_stack_backup.sh
Normal file
164
shell_portainer_stack_backup.sh
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Portainer Stack Backup (quiet/basic logging)
|
||||||
|
# Settings file: settings.env (same dir as script)
|
||||||
|
# Required settings:
|
||||||
|
# PORTAINER_URL
|
||||||
|
# API_KEY
|
||||||
|
# OUT_DIR
|
||||||
|
# CREATE_ARCHIVE (0/1)
|
||||||
|
#
|
||||||
|
# Behavior:
|
||||||
|
# - Always writes a log file in the run directory while running
|
||||||
|
# - If CREATE_ARCHIVE=1: creates RUN_DIR.tar.gz and then removes RUN_DIR
|
||||||
|
# - If CREATE_ARCHIVE=0: keeps RUN_DIR (and the log inside it)
|
||||||
|
############################################
|
||||||
|
|
||||||
|
# ---------- load settings ----------
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SETTINGS_FILE="$SCRIPT_DIR/settings.env"
|
||||||
|
|
||||||
|
if [[ -f "$SETTINGS_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$SETTINGS_FILE"
|
||||||
|
else
|
||||||
|
echo "ERROR: settings.env not found in: $SCRIPT_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
: "${PORTAINER_URL:?PORTAINER_URL not set in settings.env}"
|
||||||
|
: "${API_KEY:?API_KEY not set in settings.env}"
|
||||||
|
: "${OUT_DIR:?OUT_DIR not set in settings.env}"
|
||||||
|
: "${CREATE_ARCHIVE:?CREATE_ARCHIVE not set in settings.env}"
|
||||||
|
|
||||||
|
if [[ "$CREATE_ARCHIVE" != "0" && "$CREATE_ARCHIVE" != "1" ]]; then
|
||||||
|
echo "ERROR: CREATE_ARCHIVE must be 0 or 1 (got: '$CREATE_ARCHIVE')" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v curl >/dev/null 2>&1 || { echo "ERROR: curl missing"; exit 1; }
|
||||||
|
command -v jq >/dev/null 2>&1 || { echo "ERROR: jq missing"; exit 1; }
|
||||||
|
|
||||||
|
# tar is only required when CREATE_ARCHIVE=1
|
||||||
|
if [[ "$CREATE_ARCHIVE" == "1" ]]; then
|
||||||
|
command -v tar >/dev/null 2>&1 || { echo "ERROR: tar missing (required because CREATE_ARCHIVE=1)"; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- output + logging ----------
|
||||||
|
BASE_OUT="$OUT_DIR"
|
||||||
|
RUN_NAME="portainer-stack-backup-$(date +%F_%H%M%S)"
|
||||||
|
RUN_DIR="$BASE_OUT/$RUN_NAME"
|
||||||
|
STACKS_DIR="$RUN_DIR/stacks"
|
||||||
|
mkdir -p "$STACKS_DIR"
|
||||||
|
|
||||||
|
LOG_FILE="$RUN_DIR/backup.log"
|
||||||
|
# Mirror ALL output (stdout+stderr) to terminal + logfile
|
||||||
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||||
|
|
||||||
|
# ---------- minimal log helpers ----------
|
||||||
|
say() { echo "$*"; }
|
||||||
|
ok() { echo "[OK] $*"; }
|
||||||
|
fail() { echo "[FAIL] $*"; }
|
||||||
|
|
||||||
|
# ---------- curl defaults ----------
|
||||||
|
# -k to tolerate self-signed certs on 9443
|
||||||
|
CURL_BASE=(curl -skS)
|
||||||
|
HDR=(-H "X-API-Key: ${API_KEY}")
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Header
|
||||||
|
############################################
|
||||||
|
say "Portainer Stack Backup started"
|
||||||
|
say "Target: $RUN_DIR"
|
||||||
|
say
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Preflight
|
||||||
|
############################################
|
||||||
|
if "${CURL_BASE[@]}" "$PORTAINER_URL/api/status" >/dev/null; then
|
||||||
|
say "Portainer reachable"
|
||||||
|
else
|
||||||
|
say "Portainer not reachable"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if "${CURL_BASE[@]}" "${HDR[@]}" "$PORTAINER_URL/api/users/me" >/dev/null; then
|
||||||
|
say "API key valid"
|
||||||
|
else
|
||||||
|
say "API key invalid"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
stacks_json="$("${CURL_BASE[@]}" "${HDR[@]}" "$PORTAINER_URL/api/stacks")"
|
||||||
|
if ! echo "$stacks_json" | jq -e 'type=="array"' >/dev/null 2>&1; then
|
||||||
|
say "Unexpected response while fetching stacks (not a JSON array)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
stack_count="$(echo "$stacks_json" | jq 'length')"
|
||||||
|
say "Stacks found: $stack_count"
|
||||||
|
say
|
||||||
|
say "Starting backup"
|
||||||
|
say
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Export stacks (one line per stack)
|
||||||
|
############################################
|
||||||
|
ok_count=0
|
||||||
|
fail_count=0
|
||||||
|
|
||||||
|
while IFS= read -r s; do
|
||||||
|
id="$(echo "$s" | jq -r '.Id')"
|
||||||
|
name_raw="$(echo "$s" | jq -r '.Name')"
|
||||||
|
endpoint="$(echo "$s" | jq -r '.EndpointId')"
|
||||||
|
|
||||||
|
# sanitize name for filesystem
|
||||||
|
name_fs="$(echo "$name_raw" | tr '/ ' '__' | tr -cd '[:alnum:]_.-')"
|
||||||
|
[[ -n "$name_fs" ]] || name_fs="stack_${id}"
|
||||||
|
|
||||||
|
stack_dir="$STACKS_DIR/$name_fs"
|
||||||
|
mkdir -p "$stack_dir"
|
||||||
|
|
||||||
|
# minimal meta (helps mapping)
|
||||||
|
echo "$s" | jq '.' > "$stack_dir/meta.json"
|
||||||
|
|
||||||
|
# fetch stack file and extract StackFileContent
|
||||||
|
file_json="$("${CURL_BASE[@]}" "${HDR[@]}" \
|
||||||
|
"$PORTAINER_URL/api/stacks/$id/file?endpointId=$endpoint" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
content="$(echo "$file_json" | jq -r '.StackFileContent // empty' 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ -n "$content" && "$content" != "null" ]]; then
|
||||||
|
printf "%s\n" "$content" > "$stack_dir/docker-compose.yml"
|
||||||
|
ok "$name_raw"
|
||||||
|
ok_count=$((ok_count+1))
|
||||||
|
else
|
||||||
|
fail "$name_raw"
|
||||||
|
fail_count=$((fail_count+1))
|
||||||
|
# keep raw response for troubleshooting (quiet, but useful)
|
||||||
|
printf "%s\n" "$file_json" > "$stack_dir/_file_response.json" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done < <(echo "$stacks_json" | jq -c '.[]')
|
||||||
|
|
||||||
|
say
|
||||||
|
say "Backup finished: OK=$ok_count FAIL=$fail_count"
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Optional archive + cleanup
|
||||||
|
############################################
|
||||||
|
if [[ "$CREATE_ARCHIVE" == "1" ]]; then
|
||||||
|
tarball="${RUN_DIR}.tar.gz"
|
||||||
|
tar -C "$RUN_DIR" -czf "$tarball" .
|
||||||
|
say "Archive created: $(basename "$tarball")"
|
||||||
|
|
||||||
|
# Cleanup: remove the run directory after successful archive creation
|
||||||
|
rm -rf "$RUN_DIR"
|
||||||
|
say "Cleanup done: removed $RUN_DIR"
|
||||||
|
else
|
||||||
|
say "Keeping directory: $RUN_DIR"
|
||||||
|
say "Log: $LOG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
say "Done."
|
||||||
Reference in New Issue
Block a user