Files
homelab-infra/ops/restore-tests/nextcloud-restore-test.sh
T
Micha 53c34dca0e fix(restore): nextcloud-test disable check_data_directory_permissions
Erster Lauf am 2026-06-03 lief sauber durch alle Phasen (Borg-Extract,
pg_restore, Container alle gesund), schlug aber im HTTP-Smoke mit 503 fehl.
Ursache (aus dem preserved /mnt/user/backups/restore-lab/_failed/...):
- OC_Util.php:486 prueft die Permissions der data-Dir
- Skript hatte chmod -R a+rwX gesetzt (0777, letzte Stelle 7)
- Nextcloud versucht selbst chmod(0770) als www-data im Container
- Unraids shfs/FUSE lehnt chmod von Non-Root ab
- Nextcloud meldet "data directory readable by other people" -> 503

Fix: in der gepatchten config.php zusaetzlich
'check_data_directory_permissions' => false setzen. Nextcloud bietet
das in OC_Util:480 explizit als Opt-out an, fuer den isolierten Smoke
mit Wegwerf-Daten ist das vertretbar (kein Public, kein Traefik).
Produktiv bleibt der Check natuerlich an.

Patching erfolgt im bestehenden PHP-Injection-Block; idempotent (laeuft
keine Aenderung wenn beide Keys schon im config.php sind). Fallback-
sed-Pfad fuer Hosts ohne php ebenfalls erweitert.
2026-06-03 19:23:08 +02:00

320 lines
12 KiB
Bash

#!/bin/bash
set -euo pipefail
# Nextcloud Restore Smoke Test
#
# Nicht-destruktiver Restore-Smoke-Test fuer Nextcloud.
#
# Was dieser Smoke nachweist:
# - Nextcloud-HTML und -Datenpfade koennen aus dem Borg-Archiv extrahiert werden
# - nextcloud.dump kann in eine isolierte Test-Postgres importiert werden
# - Nextcloud startet gegen die restaurierten Daten + Test-Redis und antwortet
# auf HTTP
# - occ status zeigt maintenance:mode = false
#
# Besonderheiten gegenueber den anderen Restore-Tests:
# - Nextcloud hat eine eigene Postgres (nicht shared), mit eigener DB-Rolle
# - Nextcloud nutzt eine eigene Redis-Instanz (Snapshot-Persistenz, kein Passwort)
# - occ maintenance:mode und die Rolle oc_admin sind im DR-Fall relevant;
# im Smoke pruefen wir occ status nach dem Boot
# - Produktive Secrets (admin_user, admin_password, postgres_password) werden
# durch Wegwerf-Werte im Test-Compose ersetzt
#
# Produktive Nextcloud-Container, produktive Postgres-DB, produktive Secrets,
# produktive Nutzdaten unter /mnt/user/documents/nextcloud-data und
# produktiver Traefik-Eintrag werden NICHT angefasst.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
WHATIF=0
KEEP_DATA=0
for arg in "$@"; do
case "$arg" in
--what-if) WHATIF=1 ;;
--keep-data) KEEP_DATA=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
RESTORE_ROOT="/mnt/user/backups/restore-lab/nextcloud"
REPORT_ROOT="/mnt/user/backups/restore-reports"
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/nextcloud-extract"
COMPOSE_FILE="$SCRIPT_DIR/nextcloud-compose.test.yml"
REPORT_FILE="$REPORT_ROOT/nextcloud-$(date +%F).md"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Nextcloud restore test
Mode: WhatIf
RestoreRoot: $RESTORE_ROOT
ReportRoot: $REPORT_ROOT
Expected Borg source paths:
- local/appdata/nextcloud/html (aus Borg-Archiv)
Host source paths:
- /mnt/user/backups/borg/dumps/latest/nextcloud.dump (vom Host, taeglich frisch)
Planned isolation:
- Test-Postgres: postgres:18.4 mit Wegwerf-Credentials
- Test-Redis: redis:8.8.0-alpine (rebuildbar, kein Restore)
- Test-Nextcloud: nextcloud:33.0.4-apache (Image-Pin wie Produktion)
- Wegwerf-Admin-Credentials im Test-Compose
- Produktive Secrets und Nutzdaten werden NICHT gemountet
- Test endpoint: 127.0.0.1:18180 (no Traefik, no public domain)
Smoke-Test:
- pg_restore -> nextcloud.dump
- HTTP 200/302/3xx von 127.0.0.1:18180
- occ status: maintenance=false
EOF
exit 0
fi
require_cmd docker
require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
RESTORE_SUCCESS=0
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "nextcloud" "$RESTORE_ROOT"
rm -rf "$EXTRACT_DIR"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$RESTORE_ROOT"
fi
rm -rf "$EXTRACT_DIR"
}
trap cleanup EXIT
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
mkdir -p "$RESTORE_ROOT/html" "$RESTORE_ROOT/data" "$RESTORE_ROOT/postgres" "$RESTORE_ROOT/dumps/latest"
archive="$(latest_archive_name)"
repo="$(borg_repo_url)"
if [ -z "$archive" ] || [ -z "$repo" ]; then
echo "Could not resolve Borg repo/archive from borg-ui database" >&2
exit 1
fi
# Stufe 1: Nextcloud-App-Pfade aus Borg, Dump vom Host.
# HTML (App-Code + config) kommt aus dem Borg-Archiv.
# Der Dump liegt frisch auf dem Host unter /mnt/user/backups/borg/dumps/latest/
# (wird taeglich von pre-backup-dumps.sh erzeugt und dann in Borg gesichert).
# Der Borg-Extract des Dumps wuerde dieselbe Datei liefern, braucht aber eine
# eigene Remote-Roundtrip-Zeit; wir nutzen die Host-Kopie direkt.
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/nextcloud.dump"
borg_extract "/restore/nextcloud-extract" \
"local/appdata/nextcloud/html"
if [ ! -d "$EXTRACT_DIR/local/appdata/nextcloud/html" ]; then
echo "Nextcloud html path missing in Borg archive" >&2
exit 1
fi
if [ ! -f "$DUMP_HOST_PATH" ]; then
echo "nextcloud.dump missing on host at $DUMP_HOST_PATH" >&2
exit 1
fi
# App-Code + Config ins Restore-Lab verschieben
cp -a "$EXTRACT_DIR/local/appdata/nextcloud/html/." "$RESTORE_ROOT/html/"
cp "$DUMP_HOST_PATH" "$RESTORE_ROOT/dumps/latest/nextcloud.dump"
# Nextcloud braucht einen beschreibbaren data-Pfad, auch wenn er leer ist.
# Im Restore-Lab ist das /mnt/user/backups/restore-lab/nextcloud/data.
mkdir -p "$RESTORE_ROOT/data"
# Unraid (FUSE/shfs) ignoriert chown auf User-Shares. Stattdessen setzen
# wir die Dateien auf world-writable, damit der Nextcloud-Entrypoint
# (der als root startet und intern auf www-data wechselt) die Dateien
# lesen und beschreiben kann. Im isolierten Smoke-Kontext vertretbar.
chmod -R a+rwX "$RESTORE_ROOT/html" "$RESTORE_ROOT/data"
# Falls config.php einen anderen dbuser als das Test-Compose hat, patchen
# wir die DB-Zugangsdaten in der restaurierten config.php fuer den Test.
CONFIG_PHP="$RESTORE_ROOT/html/config/config.php"
if [ -f "$CONFIG_PHP" ]; then
# Backup der Originalkonfig fuer Diagnose
cp "$CONFIG_PHP" "$RESTORE_ROOT/html/config/config.php.original"
# DB-Credentials auf die Test-Werte umbiegen. Nextcloud config.php
# ist PHP; wir patchen die relevanten Zeilen per sed.
sed -i \
-e "s|'dbhost'.*|'dbhost' => 'restoretest-nextcloud-postgres',|" \
-e "s|'dbuser'.*|'dbuser' => 'nextcloud',|" \
-e "s|'dbpassword'.*|'dbpassword' => 'restoretest-nextcloud-db',|" \
-e "s|'dbname'.*|'dbname' => 'nextcloud',|" \
-e "s|'dbport'.*|'dbport' => '',|" \
"$CONFIG_PHP"
# Redis-Host patchen. Die config.php hat ein verschachteltes Array:
# 'redis' => array( 'host' => 'nextcloud-redis', ... )
# Wir ersetzen nur den Host-Wert innerhalb des redis-Blocks.
sed -i "s|'host' => 'nextcloud-redis'|'host' => 'restoretest-nextcloud-redis'|g" "$CONFIG_PHP"
# Zwei Patches in der config.php, beides per PHP-Code-Injection am Ende:
#
# 1. trusted_domains: 127.0.0.1 hinzufuegen, damit der Smoke-Endpunkt
# akzeptiert wird. Nextcloud prueft trusted_domains und blockt sonst
# mit "Access through untrusted domain" (503).
#
# 2. check_data_directory_permissions: false. Hintergrund: Nextcloud
# (OC_Util::checkDataDirectoryPermissions) prueft beim HTTP-Request, ob
# die data-Dir-Permissions in der letzten Stelle 0 sind. Falls nicht,
# versucht es als www-data ein chmod(0770). Auf Unraid (shfs/FUSE)
# lehnt das Filesystem chmod von Non-Root ab, also kann der Container
# das nie korrigieren -> Nextcloud meldet "data directory readable by
# other people" -> HTTP 503. Im isolierten Smoke-Kontext (Wegwerf-
# Daten, kein Public, kein Traefik) ist das Aushebeln dieses Checks
# sauber dokumentiert vorgesehen. Produktiv bleibt der Check an.
php -r "
\$f = '$CONFIG_PHP';
\$c = file_get_contents(\$f);
if (strpos(\$c, \"'127.0.0.1'\") === false || strpos(\$c, 'check_data_directory_permissions') === false) {
include \$f;
if (!in_array('127.0.0.1', \$CONFIG['trusted_domains'])) {
\$CONFIG['trusted_domains'][] = '127.0.0.1';
}
\$CONFIG['check_data_directory_permissions'] = false;
\$out = '<?php' . PHP_EOL . '\$CONFIG = ' . var_export(\$CONFIG, true) . ';' . PHP_EOL;
file_put_contents(\$f, \$out);
}
" 2>/dev/null || {
# Fallback: wenn php nicht auf dem Host ist, per sed versuchen
if ! grep -q "127.0.0.1" "$CONFIG_PHP"; then
sed -i "/'trusted_domains'/,/^ )/s|^ )| 99 => '127.0.0.1',\n )|" "$CONFIG_PHP" || true
fi
if ! grep -q "check_data_directory_permissions" "$CONFIG_PHP"; then
sed -i "s|^);| 'check_data_directory_permissions' => false,\n);|" "$CONFIG_PHP" || true
fi
}
config_patched="ok"
else
config_patched="no config.php found"
fi
# Stufe 2: Test-Postgres + Test-Redis hochfahren
docker compose -f "$COMPOSE_FILE" up -d restoretest-nextcloud-postgres restoretest-nextcloud-redis >/dev/null
until docker exec restoretest-nextcloud-postgres pg_isready -U nextcloud -d nextcloud >/dev/null 2>&1; do
sleep 2
done
# Stufe 3: Dump einspielen (mit Retry wie bei Paperless/Immich)
restore_ok=0
for attempt in $(seq 1 12); do
if docker exec -i restoretest-nextcloud-postgres \
pg_restore -U nextcloud -d nextcloud --clean --if-exists --no-owner --no-privileges \
< "$RESTORE_ROOT/dumps/latest/nextcloud.dump" 2>/tmp/nextcloud-pg-restore.err; then
restore_ok=1
break
fi
if grep -qiE "starting up|shutting down|connection refused|database .* does not exist" /tmp/nextcloud-pg-restore.err; then
sleep 5
continue
fi
# pg_restore mit --clean erzeugt "does not exist"-Warnungen fuer nicht vorhandene
# Objekte beim ersten Import. Diese sind erwartbar und kein echter Fehler.
# Wir pruefen auf harte Fehler.
if grep -qiE "FATAL|PANIC" /tmp/nextcloud-pg-restore.err; then
cat /tmp/nextcloud-pg-restore.err >&2
exit 1
fi
restore_ok=1
break
done
if [ "$restore_ok" -ne 1 ]; then
cat /tmp/nextcloud-pg-restore.err >&2
exit 1
fi
# Stufe 4: Nextcloud starten
docker compose -f "$COMPOSE_FILE" up -d restoretest-nextcloud >/dev/null
# Nextcloud braucht beim ersten Start mit existierender config.php einige
# Sekunden fuer DB-Migrations-Checks. Wir geben bis zu 180s.
http_status=""
for _ in $(seq 1 90); do
http_status="$(curl -s -o /tmp/nextcloud-body.html -w '%{http_code}' \
-L http://127.0.0.1:18180/status.php || true)"
if [ "$http_status" = "200" ]; then
break
fi
sleep 2
done
if [ "$http_status" != "200" ]; then
echo "Nextcloud HTTP smoke failed: status=$http_status" >&2
docker logs --tail 120 restoretest-nextcloud >&2 || true
exit 1
fi
# Stufe 5: occ status pruefen (maintenance mode)
occ_output="$(docker exec -u www-data restoretest-nextcloud php occ status --output=json 2>/dev/null || echo '{}')"
maintenance="$(echo "$occ_output" | grep -o '"maintenance":[a-z]*' | head -1 | cut -d: -f2)"
if [ -z "$maintenance" ]; then
maintenance="unknown"
fi
# DB-Tabellen-Count als fachlicher Sanity-Check
table_count="$(docker exec restoretest-nextcloud-postgres \
psql -U nextcloud -d nextcloud -tAc \
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" \
2>/dev/null | tr -d '[:space:]' || echo "n/a")"
write_report "$REPORT_FILE" <<EOF
# Nextcloud Restore Test Report - $(date +%F)
- Service: \`nextcloud\`
- Source repo: \`$repo\`
- Archive: \`$archive\`
- Restore root: \`$RESTORE_ROOT\`
- Test containers:
- \`restoretest-nextcloud\`
- \`restoretest-nextcloud-postgres\`
- \`restoretest-nextcloud-redis\`
- Test endpoint: \`http://127.0.0.1:18180/status.php\`
- Result: \`SUCCESS\`
## Checks
- Borg extract of html: \`ok\`
- Host dump copy: \`ok\`
- config.php patched for test DB: \`$config_patched\`
- Dump import into isolated Postgres: \`ok\`
- HTTP status from /status.php: \`$http_status\`
- occ status maintenance: \`$maintenance\`
- Public table count in test DB: \`$table_count\`
## Scope
Dieser Smoke prueft: Borg-Restore von App-Code + Config + DB-Dump,
Dump-Import in isoliertes Test-Postgres, Nextcloud-Boot mit restaurierter
config.php (DB-Credentials auf Test-Werte gepatcht), HTTP-Status und
occ-Maintenance-Status.
Bewusst NICHT Teil des Smokes:
- Voller Restore der Nutzdaten unter /mnt/user/documents/nextcloud-data
(zu gross fuer regelmaessigen Smoke; Pfad-Existenz im Archiv kann
separat geprueft werden)
- Produktive Secrets (admin_user/password, postgres_password)
- Traefik-Route und produktive Domain cloud.kaleschke.info
- occ maintenance:mode Toggle (der Test-Restore braucht keinen
vorhergehenden maintenance:mode --on, weil er gegen einen Dump laeuft)
## Notes
- Test ran without Traefik and without the productive domain.
- Productive Nextcloud secrets were NOT mounted; test uses throwaway credentials.
- Productive user data under /mnt/user/documents/nextcloud-data was NOT mounted.
- config.php.original preserved for diagnosis.
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
RESTORE_SUCCESS=1
echo "Nextcloud restore test ok -> $REPORT_FILE"