feat(ops): duman testi, yedekleme ve geri yükleme betikleri
- smoke-test.sh: sağlık, replica set, güvenlik başlıkları, HTTPS, oturumsuz erişim, giriş - butcem-backup.sh: mongodump + uploads arşivi, SHA-256, 7 gün saklama - butcem-restore.sh: bütünlük kontrolü ve onayla geri yükleme Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
# /etc/butcem-backup.env — butcem-backup.sh ayarları. İzin: sudo chmod 600 /etc/butcem-backup.env
|
||||||
|
# Değerler için docs/DEPLOY.md › Yedekleme.
|
||||||
|
|
||||||
|
# MongoDB konteyner adı veya adının ayırt edici bir parçası (docker ps --format '{{.Names}}')
|
||||||
|
MONGO_CONTAINER="butcem-mongo"
|
||||||
|
MONGO_ROOT_USER="butcem_root"
|
||||||
|
MONGO_ROOT_PASSWORD=""
|
||||||
|
|
||||||
|
# Uygulamanın /app/uploads volume adı (docker volume ls)
|
||||||
|
UPLOADS_VOLUME="butcem-uploads"
|
||||||
|
|
||||||
|
BACKUP_DIR="/var/backups/butcem"
|
||||||
|
RETENTION_DAYS="7"
|
||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# BÜTÇEM günlük yedek — sunucuda (Atlas) root olarak, cron ile çalışır. Ayrıntı: docs/DEPLOY.md › Yedekleme
|
||||||
|
#
|
||||||
|
# Üretir (BACKUP_DIR altında, tarih damgalı):
|
||||||
|
# mongo-YYYYmmdd-HHMMSS.archive.gz → butcem veritabanı (mongodump --archive --gzip)
|
||||||
|
# uploads-YYYYmmdd-HHMMSS.tar.gz → yüklenen dosyalar (uploads volume)
|
||||||
|
# SHA256SUMS-YYYYmmdd-HHMMSS → bütünlük özetleri
|
||||||
|
# RETENTION_DAYS günden eski yedekleri siler.
|
||||||
|
#
|
||||||
|
# Ayarlar /etc/butcem-backup.env dosyasından okunur (yalnızca root okuyabilmeli: chmod 600).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG="${BUTCEM_BACKUP_CONFIG:-/etc/butcem-backup.env}"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
[ -f "$CONFIG" ] && . "$CONFIG"
|
||||||
|
|
||||||
|
: "${MONGO_CONTAINER:?MONGO_CONTAINER tanımlı değil (MongoDB konteyner adı veya adının bir parçası)}"
|
||||||
|
: "${MONGO_ROOT_USER:?MONGO_ROOT_USER tanımlı değil}"
|
||||||
|
: "${MONGO_ROOT_PASSWORD:?MONGO_ROOT_PASSWORD tanımlı değil}"
|
||||||
|
: "${UPLOADS_VOLUME:?UPLOADS_VOLUME tanımlı değil (uygulamanın /app/uploads volume adı)}"
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/var/backups/butcem}"
|
||||||
|
RETENTION_DAYS="${RETENTION_DAYS:-7}"
|
||||||
|
DB_NAME="${DB_NAME:-butcem}"
|
||||||
|
HELPER_IMAGE="${HELPER_IMAGE:-alpine:3.20}"
|
||||||
|
|
||||||
|
TS="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
log() { echo "[$(date '+%F %T')] $*"; }
|
||||||
|
|
||||||
|
# Dokploy konteyner adlarına rastgele son ek ekler; adın bir parçasıyla eşleşen çalışan konteyner.
|
||||||
|
CID="$(docker ps -q --filter "name=${MONGO_CONTAINER}" | head -n1)"
|
||||||
|
[ -n "$CID" ] || { log "HATA: '${MONGO_CONTAINER}' adlı çalışan MongoDB konteyneri yok."; exit 1; }
|
||||||
|
docker volume inspect "$UPLOADS_VOLUME" >/dev/null 2>&1 || { log "HATA: '${UPLOADS_VOLUME}' volume'u yok."; exit 1; }
|
||||||
|
|
||||||
|
install -d -m 700 "$BACKUP_DIR"
|
||||||
|
MONGO_FILE="$BACKUP_DIR/mongo-$TS.archive.gz"
|
||||||
|
UPLOADS_FILE="$BACKUP_DIR/uploads-$TS.tar.gz"
|
||||||
|
|
||||||
|
# 1) MongoDB: parola komut satırında görünmesin diye geçici yapılandırma dosyasıyla.
|
||||||
|
log "MongoDB yedeği alınıyor ($DB_NAME)…"
|
||||||
|
CONF="/tmp/butcem-dump-$TS.yaml"
|
||||||
|
printf 'password: "%s"\n' "$MONGO_ROOT_PASSWORD" | docker exec -i "$CID" sh -c "umask 077 && cat > $CONF"
|
||||||
|
if ! docker exec "$CID" mongodump --quiet --config "$CONF" --username "$MONGO_ROOT_USER" \
|
||||||
|
--authenticationDatabase admin --db "$DB_NAME" --archive --gzip >"$MONGO_FILE"; then
|
||||||
|
docker exec "$CID" rm -f "$CONF"
|
||||||
|
rm -f "$MONGO_FILE"
|
||||||
|
log "HATA: mongodump başarısız."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker exec "$CID" rm -f "$CONF"
|
||||||
|
[ -s "$MONGO_FILE" ] || { log "HATA: MongoDB yedeği boş."; exit 1; }
|
||||||
|
|
||||||
|
# 2) Yüklenen dosyalar: volume salt okunur bağlanır.
|
||||||
|
log "Uploads yedeği alınıyor ($UPLOADS_VOLUME)…"
|
||||||
|
docker run --rm -v "$UPLOADS_VOLUME":/data:ro -v "$BACKUP_DIR":/backup "$HELPER_IMAGE" \
|
||||||
|
tar czf "/backup/$(basename "$UPLOADS_FILE")" -C /data .
|
||||||
|
[ -s "$UPLOADS_FILE" ] || { log "HATA: uploads yedeği boş."; exit 1; }
|
||||||
|
|
||||||
|
# 3) Bütünlük özeti ve izinler
|
||||||
|
( cd "$BACKUP_DIR" && sha256sum "$(basename "$MONGO_FILE")" "$(basename "$UPLOADS_FILE")" >"SHA256SUMS-$TS" )
|
||||||
|
chmod 600 "$MONGO_FILE" "$UPLOADS_FILE" "$BACKUP_DIR/SHA256SUMS-$TS"
|
||||||
|
|
||||||
|
# 4) Saklama süresi
|
||||||
|
find "$BACKUP_DIR" -maxdepth 1 -type f \( -name 'mongo-*.archive.gz' -o -name 'uploads-*.tar.gz' -o -name 'SHA256SUMS-*' \) \
|
||||||
|
-mtime +"$RETENTION_DAYS" -print -delete | sed 's/^/ silindi: /'
|
||||||
|
|
||||||
|
log "Tamam: $(du -h "$MONGO_FILE" | cut -f1) veritabanı, $(du -h "$UPLOADS_FILE" | cut -f1) dosya → $BACKUP_DIR"
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# BÜTÇEM geri yükleme — sunucuda root olarak, ELLE çalıştırılır. Ayrıntı: docs/DEPLOY.md › Geri yükleme
|
||||||
|
#
|
||||||
|
# sudo scripts/backup/butcem-restore.sh /var/backups/butcem/mongo-….archive.gz [/var/backups/butcem/uploads-….tar.gz]
|
||||||
|
#
|
||||||
|
# DİKKAT: butcem veritabanındaki koleksiyonlar yedektekilerle DEĞİŞTİRİLİR (--drop); uploads arşivi
|
||||||
|
# verilirse volume içeriği silinip arşivden yazılır. Önce uygulamayı durdurun (Dokploy › Stop).
|
||||||
|
# Ayarlar butcem-backup.sh ile aynı dosyadan: /etc/butcem-backup.env
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG="${BUTCEM_BACKUP_CONFIG:-/etc/butcem-backup.env}"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
[ -f "$CONFIG" ] && . "$CONFIG"
|
||||||
|
: "${MONGO_CONTAINER:?}" "${MONGO_ROOT_USER:?}" "${MONGO_ROOT_PASSWORD:?}" "${UPLOADS_VOLUME:?}"
|
||||||
|
DB_NAME="${DB_NAME:-butcem}"
|
||||||
|
HELPER_IMAGE="${HELPER_IMAGE:-alpine:3.20}"
|
||||||
|
|
||||||
|
MONGO_FILE="${1:?Kullanım: butcem-restore.sh <mongo-arşivi> [uploads-arşivi]}"
|
||||||
|
UPLOADS_FILE="${2:-}"
|
||||||
|
[ -s "$MONGO_FILE" ] || { echo "Arşiv yok veya boş: $MONGO_FILE"; exit 1; }
|
||||||
|
[ -z "$UPLOADS_FILE" ] || [ -s "$UPLOADS_FILE" ] || { echo "Arşiv yok veya boş: $UPLOADS_FILE"; exit 1; }
|
||||||
|
|
||||||
|
# Bütünlük kontrolü (aynı klasörde SHA256SUMS varsa)
|
||||||
|
DIR="$(cd "$(dirname "$MONGO_FILE")" && pwd)"
|
||||||
|
STAMP="$(basename "$MONGO_FILE" | sed -E 's/^mongo-(.*)\.archive\.gz$/\1/')"
|
||||||
|
if [ -f "$DIR/SHA256SUMS-$STAMP" ]; then
|
||||||
|
(cd "$DIR" && sha256sum -c "SHA256SUMS-$STAMP") || { echo "Bütünlük kontrolü başarısız."; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
CID="$(docker ps -q --filter "name=${MONGO_CONTAINER}" | head -n1)"
|
||||||
|
[ -n "$CID" ] || { echo "Çalışan MongoDB konteyneri yok: $MONGO_CONTAINER"; exit 1; }
|
||||||
|
|
||||||
|
echo "Geri yüklenecek: $MONGO_FILE${UPLOADS_FILE:+ ve $UPLOADS_FILE}"
|
||||||
|
echo "'$DB_NAME' veritabanındaki mevcut veri yedektekiyle DEĞİŞTİRİLECEK."
|
||||||
|
if [ "${ASSUME_YES:-0}" != "1" ]; then
|
||||||
|
read -r -p "Devam etmek için 'evet' yazın: " ANSWER
|
||||||
|
[ "$ANSWER" = "evet" ] || { echo "Vazgeçildi."; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONF="/tmp/butcem-restore-$$.yaml"
|
||||||
|
printf 'password: "%s"\n' "$MONGO_ROOT_PASSWORD" | docker exec -i "$CID" sh -c "umask 077 && cat > $CONF"
|
||||||
|
trap 'docker exec "$CID" rm -f "$CONF" >/dev/null 2>&1 || true' EXIT
|
||||||
|
docker exec -i "$CID" mongorestore --quiet --config "$CONF" --username "$MONGO_ROOT_USER" \
|
||||||
|
--authenticationDatabase admin --archive --gzip --drop --nsInclude "${DB_NAME}.*" <"$MONGO_FILE"
|
||||||
|
echo "✓ Veritabanı geri yüklendi."
|
||||||
|
|
||||||
|
if [ -n "$UPLOADS_FILE" ]; then
|
||||||
|
docker run --rm -v "$UPLOADS_VOLUME":/data -v "$(cd "$(dirname "$UPLOADS_FILE")" && pwd)":/backup:ro "$HELPER_IMAGE" \
|
||||||
|
sh -c "find /data -mindepth 1 -delete && tar xzf /backup/$(basename "$UPLOADS_FILE") -C /data && chown -R 1001:1001 /data"
|
||||||
|
echo "✓ Yüklenen dosyalar geri yüklendi."
|
||||||
|
fi
|
||||||
|
echo "Uygulamayı yeniden başlatın (Dokploy › Start) ve duman testini çalıştırın."
|
||||||
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# BÜTÇEM duman testi (smoke test) — canlıya alma sonrası, yalnızca OKUMA yapar; veri değiştirmez.
|
||||||
|
#
|
||||||
|
# Kullanım:
|
||||||
|
# scripts/smoke-test.sh https://butcem.ta1sph.website [beklenen-sürüm]
|
||||||
|
# SMOKE_USER=admin SMOKE_PASSWORD='…' scripts/smoke-test.sh https://butcem.ta1sph.website 1.0.0
|
||||||
|
#
|
||||||
|
# Gerekenler: bash, curl. Çıkış kodu: 0 = hepsi geçti, 1 = en az bir kontrol başarısız.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
BASE="${1:?Kullanım: scripts/smoke-test.sh <adres> [beklenen-sürüm]}"
|
||||||
|
BASE="${BASE%/}"
|
||||||
|
EXPECT_VERSION="${2:-}"
|
||||||
|
FAIL=0
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
|
||||||
|
bad() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; }
|
||||||
|
check() { if eval "$2"; then ok "$1"; else bad "$1"; fi; }
|
||||||
|
|
||||||
|
echo "BÜTÇEM duman testi: $BASE"
|
||||||
|
|
||||||
|
echo "1) Sağlık kontrolü"
|
||||||
|
HEALTH_CODE=$(curl -s -o "$TMP/health" -w '%{http_code}' "$BASE/api/health")
|
||||||
|
HEALTH=$(cat "$TMP/health")
|
||||||
|
echo " $HEALTH"
|
||||||
|
check "GET /api/health 200" '[ "$HEALTH_CODE" = 200 ]'
|
||||||
|
check "veritabanı erişilebilir" 'grep -q "\"db\":\"ok\"" <<<"$HEALTH"'
|
||||||
|
check "MongoDB replica set (transaction için)" '! grep -q "\"replicaSet\":null" <<<"$HEALTH" && grep -q "\"replicaSet\":\"" <<<"$HEALTH"'
|
||||||
|
if [ -n "$EXPECT_VERSION" ]; then
|
||||||
|
check "sürüm $EXPECT_VERSION" 'grep -q "\"version\":\"$EXPECT_VERSION\"" <<<"$HEALTH"'
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "2) Sayfalar ve güvenlik başlıkları"
|
||||||
|
LOGIN_CODE=$(curl -s -D "$TMP/login-headers" -o /dev/null -w '%{http_code}' "$BASE/login")
|
||||||
|
H=$(tr -d '\r' <"$TMP/login-headers" | tr 'A-Z' 'a-z')
|
||||||
|
check "GET /login 200" '[ "$LOGIN_CODE" = 200 ]'
|
||||||
|
check "Content-Security-Policy var" 'grep -q "^content-security-policy:" <<<"$H"'
|
||||||
|
check "X-Frame-Options: DENY" 'grep -q "^x-frame-options: deny" <<<"$H"'
|
||||||
|
check "X-Content-Type-Options: nosniff" 'grep -q "^x-content-type-options: nosniff" <<<"$H"'
|
||||||
|
if [[ "$BASE" == https://* ]]; then
|
||||||
|
check "Strict-Transport-Security var" 'grep -q "^strict-transport-security:" <<<"$H"'
|
||||||
|
HTTP_URL="http://${BASE#https://}/login"
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$HTTP_URL")
|
||||||
|
check "HTTP → HTTPS yönlendirmesi ($HTTP_CODE)" '[[ "$HTTP_CODE" =~ ^30[1278]$ ]]'
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "3) Oturumsuz erişim engelleniyor"
|
||||||
|
DASH=$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' "$BASE/dashboard")
|
||||||
|
check "/dashboard → giriş sayfasına yönlendirme ($DASH)" '[[ "$DASH" =~ ^30[27]\ .*/login ]]'
|
||||||
|
FILE_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/files/000000000000000000000000")
|
||||||
|
check "/api/files oturumsuz 401 ($FILE_CODE)" '[ "$FILE_CODE" = 401 ]'
|
||||||
|
CSV=$(curl -s -o "$TMP/csv" -w '%{http_code}' "$BASE/raporlar/kategoriler/csv")
|
||||||
|
check "rapor CSV'si oturumsuz veri vermiyor ($CSV)" '! grep -q "Toplam gidere oran" "$TMP/csv"'
|
||||||
|
|
||||||
|
if [ -n "${SMOKE_USER:-}" ] && [ -n "${SMOKE_PASSWORD:-}" ]; then
|
||||||
|
echo "4) Giriş ($SMOKE_USER)"
|
||||||
|
SIGN=$(curl -s -c "$TMP/jar" -o "$TMP/signin" -w '%{http_code}' -X POST "$BASE/api/auth/sign-in/username" \
|
||||||
|
-H 'Content-Type: application/json' -H "Origin: $BASE" \
|
||||||
|
--data "$(printf '{"username":"%s","password":"%s"}' "$SMOKE_USER" "$SMOKE_PASSWORD")")
|
||||||
|
check "giriş 200 ($SIGN)" '[ "$SIGN" = 200 ]'
|
||||||
|
PAGE=$(curl -s -b "$TMP/jar" -L -o /dev/null -w '%{http_code} %{url_effective}' "$BASE/dashboard")
|
||||||
|
check "oturumla ana sayfa açılıyor ($PAGE)" '[[ "$PAGE" =~ ^200\ ]] && [[ ! "$PAGE" =~ /login ]]'
|
||||||
|
curl -s -b "$TMP/jar" -X POST "$BASE/api/auth/sign-out" -H "Origin: $BASE" -o /dev/null
|
||||||
|
else
|
||||||
|
echo "4) Giriş testi atlandı (SMOKE_USER ve SMOKE_PASSWORD tanımlı değil)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$FAIL" = 0 ]; then echo "Sonuç: tüm kontroller geçti."; else echo "Sonuç: başarısız kontrol var."; fi
|
||||||
|
exit "$FAIL"
|
||||||
Reference in New Issue
Block a user