Fix Athena recovery and installation consistency
This commit is contained in:
+6
-6
@@ -4,7 +4,7 @@ MODEL_DIR=/data/models
|
|||||||
ROUTER_API_KEY=GENERATED_BY_INSTALLER
|
ROUTER_API_KEY=GENERATED_BY_INSTALLER
|
||||||
CONTROLLER_TOKEN=GENERATED_BY_INSTALLER
|
CONTROLLER_TOKEN=GENERATED_BY_INSTALLER
|
||||||
FLUX_MODEL_DIR=/data/models/FLUX.2-klein-4B
|
FLUX_MODEL_DIR=/data/models/FLUX.2-klein-4B
|
||||||
IMAGE_GPU_DEVICES=1
|
IMAGE_GPU_DEVICES=GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe
|
||||||
PIPER_TTS_VERSION=1.6.0
|
PIPER_TTS_VERSION=1.6.0
|
||||||
PIPER_VOICE=de_DE-thorsten-high
|
PIPER_VOICE=de_DE-thorsten-high
|
||||||
XTTS_IMAGE=ghcr.io/coqui-ai/xtts-streaming-server:latest-cuda121@sha256:f7fb3b1f9d4bc88af94da1b5959d8002f1e0b003c97557164034eb8a29f01b90
|
XTTS_IMAGE=ghcr.io/coqui-ai/xtts-streaming-server:latest-cuda121@sha256:f7fb3b1f9d4bc88af94da1b5959d8002f1e0b003c97557164034eb8a29f01b90
|
||||||
@@ -36,14 +36,14 @@ ULTRA_UBATCH_SIZE=128
|
|||||||
UNCENSORED_CONTEXT=80000
|
UNCENSORED_CONTEXT=80000
|
||||||
UNCENSORED_BATCH_SIZE=2048
|
UNCENSORED_BATCH_SIZE=2048
|
||||||
UNCENSORED_UBATCH_SIZE=128
|
UNCENSORED_UBATCH_SIZE=128
|
||||||
FAST_GPU_DEVICES=0,1
|
FAST_GPU_DEVICES=GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe,GPU-4834d9d7-5b61-3004-1fb3-4ae49d482d4b
|
||||||
MEDIUM_GPU_DEVICES=0,1
|
MEDIUM_GPU_DEVICES=GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe,GPU-4834d9d7-5b61-3004-1fb3-4ae49d482d4b
|
||||||
MEDIUM_TENSOR_SPLIT=85,15
|
MEDIUM_TENSOR_SPLIT=85,15
|
||||||
LARGE_GPU_DEVICES=0,1
|
LARGE_GPU_DEVICES=GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe,GPU-4834d9d7-5b61-3004-1fb3-4ae49d482d4b
|
||||||
LARGE_TENSOR_SPLIT=86,14
|
LARGE_TENSOR_SPLIT=86,14
|
||||||
ULTRA_GPU_DEVICES=0,1
|
ULTRA_GPU_DEVICES=GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe,GPU-4834d9d7-5b61-3004-1fb3-4ae49d482d4b
|
||||||
ULTRA_TENSOR_SPLIT=80,20
|
ULTRA_TENSOR_SPLIT=80,20
|
||||||
UNCENSORED_GPU_DEVICES=0,1
|
UNCENSORED_GPU_DEVICES=GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe,GPU-4834d9d7-5b61-3004-1fb3-4ae49d482d4b
|
||||||
UNCENSORED_TENSOR_SPLIT=90,10
|
UNCENSORED_TENSOR_SPLIT=90,10
|
||||||
UNCENSORED_MTP_MAX=2
|
UNCENSORED_MTP_MAX=2
|
||||||
LLAMA_THREADS=6
|
LLAMA_THREADS=6
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ Nach einer frischen Debian-Installation und erneut eingehängtem `/data`:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ./install.sh --config /root/mike-ai-install.env
|
sudo ./install.sh --config /root/mike-ai-install.env
|
||||||
|
sudo ./restore.sh --check /data/docker-backups/athena-latest.tar.gz
|
||||||
sudo ./restore.sh /data/docker-backups/athena-latest.tar.gz
|
sudo ./restore.sh /data/docker-backups/athena-latest.tar.gz
|
||||||
sudo ./smoke-test.sh
|
sudo ./smoke-test.sh
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -12,9 +12,12 @@ INSTALL_NVIDIA_DRIVER=true
|
|||||||
# festlegen (z. B. 610). Leer lassen, um dem aktuellen stabilen Zweig zu folgen.
|
# festlegen (z. B. 610). Leer lassen, um dem aktuellen stabilen Zweig zu folgen.
|
||||||
NVIDIA_DRIVER_BRANCH=
|
NVIDIA_DRIVER_BRANCH=
|
||||||
NVIDIA_MIN_DRIVER_MAJOR=570
|
NVIDIA_MIN_DRIVER_MAJOR=570
|
||||||
TEXT_GPU_DEVICES=0
|
# Stable UUID order: CUDA0 = RTX 5080, CUDA1 = RTX 3060. Positional host
|
||||||
SECONDARY_GPU_DEVICES=1
|
# indices are intentionally avoided because nvidia-smi currently enumerates
|
||||||
IMAGE_GPU_DEVICES=1
|
# the cards in the opposite order.
|
||||||
|
TEXT_GPU_DEVICES=GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe
|
||||||
|
SECONDARY_GPU_DEVICES=GPU-4834d9d7-5b61-3004-1fb3-4ae49d482d4b
|
||||||
|
IMAGE_GPU_DEVICES=GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe
|
||||||
FLUX_MODEL_DIR=/data/models/FLUX.2-klein-4B
|
FLUX_MODEL_DIR=/data/models/FLUX.2-klein-4B
|
||||||
|
|
||||||
# Headless remote reachability. Firmware power-loss recovery is configured
|
# Headless remote reachability. Firmware power-loss recovery is configured
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import io
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class RecoveryScriptTests(unittest.TestCase):
|
||||||
|
def _archive(self, members: list[str]) -> Path:
|
||||||
|
tmp = tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False)
|
||||||
|
tmp.close()
|
||||||
|
archive = Path(tmp.name)
|
||||||
|
with tarfile.open(archive, "w:gz") as handle:
|
||||||
|
for name in members:
|
||||||
|
payload = b"test\n"
|
||||||
|
info = tarfile.TarInfo(name)
|
||||||
|
info.size = len(payload)
|
||||||
|
handle.addfile(info, io.BytesIO(payload))
|
||||||
|
self.addCleanup(archive.unlink, missing_ok=True)
|
||||||
|
return archive
|
||||||
|
|
||||||
|
def test_current_backup_layout_is_accepted(self) -> None:
|
||||||
|
archive = self._archive([
|
||||||
|
"/backup/etc-mike-ai/stack.env",
|
||||||
|
"/backup/volumes/router-state/state.json",
|
||||||
|
])
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(ROOT / "restore.sh"), "--check", str(archive)],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn("ATHENA_BACKUP_CHECK_OK", result.stdout)
|
||||||
|
|
||||||
|
def test_unexpected_tree_is_rejected(self) -> None:
|
||||||
|
archive = self._archive([
|
||||||
|
"/backup/etc-mike-ai/stack.env",
|
||||||
|
"/backup/volumes/router-state/state.json",
|
||||||
|
"/etc/shadow",
|
||||||
|
])
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(ROOT / "restore.sh"), "--check", str(archive)],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("Unerwarteter Pfad", result.stderr)
|
||||||
|
|
||||||
|
def test_backup_prefix_extracts_to_restore_layout(self) -> None:
|
||||||
|
archive = self._archive([
|
||||||
|
"/backup/etc-mike-ai/stack.env",
|
||||||
|
"/backup/volumes/router-state/state.json",
|
||||||
|
])
|
||||||
|
with tempfile.TemporaryDirectory() as target:
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"tar", "-xzf", str(archive), "-C", target,
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
root = Path(target, "backup")
|
||||||
|
self.assertTrue(Path(root, "etc-mike-ai", "stack.env").is_file())
|
||||||
|
self.assertTrue(Path(root, "volumes", "router-state", "state.json").is_file())
|
||||||
|
|
||||||
|
def test_installer_uses_current_profiles(self) -> None:
|
||||||
|
installer = (ROOT / "install.sh").read_text(encoding="utf-8")
|
||||||
|
self.assertNotIn("llama-experimental", installer)
|
||||||
|
for profile in ("fast", "medium", "large", "ultra", "uncensored"):
|
||||||
|
self.assertIn(f"llama-{profile}", installer)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Aktueller temporärer Laufzustand
|
# Aktueller produktiver Laufzustand
|
||||||
|
|
||||||
Stand: 31. August 2026
|
Stand: 1. September 2026
|
||||||
|
|
||||||
## Produktive llama.cpp-Runtime
|
## Produktive llama.cpp-Runtime
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ beendet.
|
|||||||
Dabei gilt ausdrücklich:
|
Dabei gilt ausdrücklich:
|
||||||
|
|
||||||
- Der Gesamtkontext bleibt bei **160.000 Tokens**.
|
- Der Gesamtkontext bleibt bei **160.000 Tokens**.
|
||||||
|
- Das Profil verwendet wieder **einen Slot**. Die getestete Zwei-Slot-Variante
|
||||||
|
ist nicht produktiv.
|
||||||
- **MTP / Speculative Decoding bleibt aktiviert**; MTP wurde nicht entfernt.
|
- **MTP / Speculative Decoding bleibt aktiviert**; MTP wurde nicht entfernt.
|
||||||
- Modell, Quantisierung, GPU-Aufteilung und KV-Cache-Quantisierung bleiben
|
- Modell, Quantisierung, GPU-Aufteilung und KV-Cache-Quantisierung bleiben
|
||||||
unverändert.
|
unverändert.
|
||||||
|
|||||||
@@ -21,9 +21,12 @@ Full-context results with the selected settings:
|
|||||||
|
|
||||||
The larger logical batches 3072 and 4096 did not improve Medium at ubatch 128. The original one-slot benchmark selected 2048 / 128 at 90:10.
|
The larger logical batches 3072 and 4096 did not improve Medium at ubatch 128. The original one-slot benchmark selected 2048 / 128 at 90:10.
|
||||||
|
|
||||||
## Medium two-slot benchmark
|
## Historischer Medium-Zwei-Slot-Test
|
||||||
|
|
||||||
Medium now uses two parallel slots with unified KV, so both chats dynamically share one total 160K-token pool. The model weights remain loaded only once. To fit the additional scheduler buffers, the production GPU split is 85:15 while batch / ubatch remains 2048 / 128.
|
This was an A/B candidate, not the current production configuration. Production
|
||||||
|
was returned to **one slot** because concurrent Hermes requests did not behave
|
||||||
|
reliably enough. The 85:15 GPU split and batch / ubatch 2048 / 128 remain in
|
||||||
|
production because they also work with the single-slot profile.
|
||||||
|
|
||||||
Identical fresh 100,297-token prompt with a deterministic 256-token completion:
|
Identical fresh 100,297-token prompt with a deterministic 256-token completion:
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -33,10 +33,16 @@ Router, Modelle oder Dashboard-Daten zu verlieren.
|
|||||||
4. Letztes Datenarchiv einspielen:
|
4. Letztes Datenarchiv einspielen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
sudo ./restore.sh --check /data/docker-backups/athena-latest.tar.gz
|
||||||
sudo ./restore.sh /data/docker-backups/athena-latest.tar.gz
|
sudo ./restore.sh /data/docker-backups/athena-latest.tar.gz
|
||||||
sudo ./smoke-test.sh
|
sudo ./smoke-test.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--check` liest das komplette gzip-Archiv und prüft dessen sichere
|
||||||
|
`/backup`-Struktur sowie die benötigten Konfigurations- und Volume-Bäume, ohne
|
||||||
|
Container oder Dateien zu verändern. Der reguläre Restore extrahiert und
|
||||||
|
verwendet anschließend ausschließlich diesen einen geprüften Baum.
|
||||||
|
|
||||||
Das Restore verändert weder SSH noch LAN, WireGuard, Kernel, Partitionen oder
|
Das Restore verändert weder SSH noch LAN, WireGuard, Kernel, Partitionen oder
|
||||||
Mounts.
|
Mounts.
|
||||||
|
|
||||||
@@ -58,7 +64,7 @@ Appdata-Restore plus Neuerstellung über die jeweilige Template-XML.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose --env-file /etc/mike-ai/stack.env ps
|
docker compose --env-file /etc/mike-ai/stack.env ps
|
||||||
test -s /data/docker-backups/athena-latest.tar.gz
|
sudo ./restore.sh --check /data/docker-backups/athena-latest.tar.gz
|
||||||
curl -fsS http://192.168.1.212:8099/health
|
curl -fsS http://192.168.1.212:8099/health
|
||||||
sudo ./smoke-test.sh
|
sudo ./smoke-test.sh
|
||||||
```
|
```
|
||||||
|
|||||||
+21
-1
@@ -42,6 +42,9 @@ required=(AI_HOSTNAME ADMIN_USER MODEL_DIR FAST_MODEL_FILE
|
|||||||
FAST_MODEL_URL FAST_MODEL_SHA256 MEDIUM_MODEL_FILE MEDIUM_MODEL_URL
|
FAST_MODEL_URL FAST_MODEL_SHA256 MEDIUM_MODEL_FILE MEDIUM_MODEL_URL
|
||||||
MEDIUM_MODEL_SHA256 LARGE_MODEL_FILE LARGE_MODEL_URL LARGE_MODEL_SHA256
|
MEDIUM_MODEL_SHA256 LARGE_MODEL_FILE LARGE_MODEL_URL LARGE_MODEL_SHA256
|
||||||
ULTRA_MODEL_FILE ULTRA_MODEL_URL ULTRA_MODEL_SHA256
|
ULTRA_MODEL_FILE ULTRA_MODEL_URL ULTRA_MODEL_SHA256
|
||||||
|
UNCENSORED_MODEL_FILE UNCENSORED_MODEL_URL UNCENSORED_MODEL_SHA256
|
||||||
|
UNCENSORED_PROJECTOR_FILE UNCENSORED_PROJECTOR_URL
|
||||||
|
UNCENSORED_PROJECTOR_SHA256
|
||||||
VISION_PROJECTOR_FILE VISION_PROJECTOR_URL VISION_PROJECTOR_SHA256)
|
VISION_PROJECTOR_FILE VISION_PROJECTOR_URL VISION_PROJECTOR_SHA256)
|
||||||
for name in "${required[@]}"; do
|
for name in "${required[@]}"; do
|
||||||
[[ -n "${!name:-}" ]] || die "Pflichtwert $name fehlt."
|
[[ -n "${!name:-}" ]] || die "Pflichtwert $name fehlt."
|
||||||
@@ -322,10 +325,25 @@ UNCENSORED_MODEL_FILE=$UNCENSORED_MODEL_FILE
|
|||||||
UNCENSORED_PROJECTOR_FILE=$UNCENSORED_PROJECTOR_FILE
|
UNCENSORED_PROJECTOR_FILE=$UNCENSORED_PROJECTOR_FILE
|
||||||
VISION_PROJECTOR_FILE=$VISION_PROJECTOR_FILE
|
VISION_PROJECTOR_FILE=$VISION_PROJECTOR_FILE
|
||||||
FAST_CONTEXT=${FAST_CONTEXT:-76800}
|
FAST_CONTEXT=${FAST_CONTEXT:-76800}
|
||||||
|
FAST_BATCH_SIZE=${FAST_BATCH_SIZE:-64}
|
||||||
|
FAST_UBATCH_SIZE=${FAST_UBATCH_SIZE:-32}
|
||||||
|
FAST_PARALLEL_SLOTS=${FAST_PARALLEL_SLOTS:-1}
|
||||||
MEDIUM_CONTEXT=${MEDIUM_CONTEXT:-160000}
|
MEDIUM_CONTEXT=${MEDIUM_CONTEXT:-160000}
|
||||||
|
MEDIUM_BATCH_SIZE=${MEDIUM_BATCH_SIZE:-2048}
|
||||||
|
MEDIUM_UBATCH_SIZE=${MEDIUM_UBATCH_SIZE:-128}
|
||||||
|
MEDIUM_PARALLEL_SLOTS=${MEDIUM_PARALLEL_SLOTS:-1}
|
||||||
LARGE_CONTEXT=${LARGE_CONTEXT:-192000}
|
LARGE_CONTEXT=${LARGE_CONTEXT:-192000}
|
||||||
|
LARGE_BATCH_SIZE=${LARGE_BATCH_SIZE:-2048}
|
||||||
|
LARGE_UBATCH_SIZE=${LARGE_UBATCH_SIZE:-128}
|
||||||
|
LARGE_PARALLEL_SLOTS=${LARGE_PARALLEL_SLOTS:-1}
|
||||||
ULTRA_CONTEXT=${ULTRA_CONTEXT:-262144}
|
ULTRA_CONTEXT=${ULTRA_CONTEXT:-262144}
|
||||||
|
ULTRA_BATCH_SIZE=${ULTRA_BATCH_SIZE:-2048}
|
||||||
|
ULTRA_UBATCH_SIZE=${ULTRA_UBATCH_SIZE:-128}
|
||||||
|
ULTRA_PARALLEL_SLOTS=${ULTRA_PARALLEL_SLOTS:-1}
|
||||||
UNCENSORED_CONTEXT=${UNCENSORED_CONTEXT:-80000}
|
UNCENSORED_CONTEXT=${UNCENSORED_CONTEXT:-80000}
|
||||||
|
UNCENSORED_BATCH_SIZE=${UNCENSORED_BATCH_SIZE:-2048}
|
||||||
|
UNCENSORED_UBATCH_SIZE=${UNCENSORED_UBATCH_SIZE:-128}
|
||||||
|
UNCENSORED_PARALLEL_SLOTS=${UNCENSORED_PARALLEL_SLOTS:-1}
|
||||||
FAST_GPU_DEVICES=${TEXT_GPU_DEVICES:-0}${SECONDARY_GPU_DEVICES:+,$SECONDARY_GPU_DEVICES}
|
FAST_GPU_DEVICES=${TEXT_GPU_DEVICES:-0}${SECONDARY_GPU_DEVICES:+,$SECONDARY_GPU_DEVICES}
|
||||||
MEDIUM_GPU_DEVICES=${TEXT_GPU_DEVICES:-0}${SECONDARY_GPU_DEVICES:+,$SECONDARY_GPU_DEVICES}
|
MEDIUM_GPU_DEVICES=${TEXT_GPU_DEVICES:-0}${SECONDARY_GPU_DEVICES:+,$SECONDARY_GPU_DEVICES}
|
||||||
MEDIUM_TENSOR_SPLIT=${MEDIUM_TENSOR_SPLIT:-90,10}
|
MEDIUM_TENSOR_SPLIT=${MEDIUM_TENSOR_SPLIT:-90,10}
|
||||||
@@ -340,6 +358,8 @@ IMAGE_GPU_DEVICES=${IMAGE_GPU_DEVICES:-${TEXT_GPU_DEVICES:-0}}
|
|||||||
FLUX_MODEL_DIR=${FLUX_MODEL_DIR:-/data/models/FLUX.2-klein-4B}
|
FLUX_MODEL_DIR=${FLUX_MODEL_DIR:-/data/models/FLUX.2-klein-4B}
|
||||||
LLAMA_THREADS=${LLAMA_THREADS:-6}
|
LLAMA_THREADS=${LLAMA_THREADS:-6}
|
||||||
LLAMA_THREADS_BATCH=${LLAMA_THREADS_BATCH:-6}
|
LLAMA_THREADS_BATCH=${LLAMA_THREADS_BATCH:-6}
|
||||||
|
LLAMA_CACHE_RAM_MIB=${LLAMA_CACHE_RAM_MIB:-24576}
|
||||||
|
DEFAULT_REASONING_EFFORT=${DEFAULT_REASONING_EFFORT:-off}
|
||||||
EOF
|
EOF
|
||||||
chmod 0600 $SECRETS_DIR/stack.env
|
chmod 0600 $SECRETS_DIR/stack.env
|
||||||
}
|
}
|
||||||
@@ -468,7 +488,7 @@ build_and_start() {
|
|||||||
# Portable MCPs and Hermes live on Unraid and are restored through Appdata.
|
# Portable MCPs and Hermes live on Unraid and are restored through Appdata.
|
||||||
"$STACK_DIR/platform/mcp/install-tools.sh"
|
"$STACK_DIR/platform/mcp/install-tools.sh"
|
||||||
docker compose --env-file "$SECRETS_DIR/stack.env" --profile inference create \
|
docker compose --env-file "$SECRETS_DIR/stack.env" --profile inference create \
|
||||||
llama-fast llama-medium llama-large llama-ultra llama-experimental
|
llama-fast llama-medium llama-large llama-ultra llama-uncensored
|
||||||
docker compose --env-file "$SECRETS_DIR/stack.env" --profile image create image-worker
|
docker compose --env-file "$SECRETS_DIR/stack.env" --profile image create image-worker
|
||||||
docker compose --env-file "$SECRETS_DIR/stack.env" up -d --build \
|
docker compose --env-file "$SECRETS_DIR/stack.env" up -d --build \
|
||||||
wireguard-gateway xtts piper tts-gateway profile-controller router llama-dashboard backup
|
wireguard-gateway xtts piper tts-gateway profile-controller router llama-dashboard backup
|
||||||
|
|||||||
+45
-11
@@ -4,23 +4,57 @@ set -Eeuo pipefail
|
|||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CHECK_ONLY=false
|
||||||
|
if [[ ${1:-} == --check ]]; then
|
||||||
|
CHECK_ONLY=true
|
||||||
|
shift
|
||||||
|
fi
|
||||||
ARCHIVE=${1:-/data/docker-backups/athena-latest.tar.gz}
|
ARCHIVE=${1:-/data/docker-backups/athena-latest.tar.gz}
|
||||||
|
|
||||||
die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; }
|
||||||
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen."
|
|
||||||
[[ -s $ARCHIVE ]] || die "Backup fehlt: $ARCHIVE"
|
[[ -s $ARCHIVE ]] || die "Backup fehlt: $ARCHIVE"
|
||||||
|
|
||||||
|
validate_archive() {
|
||||||
|
local listing normalized entry required
|
||||||
|
gzip -t "$ARCHIVE" || die "Backup ist beschädigt: $ARCHIVE"
|
||||||
|
listing=$(tar -tzf "$ARCHIVE") || die "Backup-Inhalt kann nicht gelesen werden."
|
||||||
|
|
||||||
|
# docker-volume-backup stores the mounted sources below /backup. GNU tar
|
||||||
|
# removes the leading slash while extracting; accept exactly that tree and
|
||||||
|
# reject every unrelated or traversing member before extracting as root.
|
||||||
|
while IFS= read -r entry; do
|
||||||
|
normalized=${entry#./}
|
||||||
|
normalized=${normalized#/}
|
||||||
|
[[ $normalized == backup || $normalized == backup/* ]] || \
|
||||||
|
die "Unerwarteter Pfad im Backup: $entry"
|
||||||
|
[[ /$normalized/ != */../* ]] || die "Unsicherer Pfad im Backup: $entry"
|
||||||
|
done <<<"$listing"
|
||||||
|
|
||||||
|
for required in backup/etc-mike-ai backup/volumes; do
|
||||||
|
grep -Eq "^/?${required}(/|$)" <<<"$listing" || \
|
||||||
|
die "Backup enthält $required nicht."
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_archive
|
||||||
|
if [[ $CHECK_ONLY == true ]]; then
|
||||||
|
printf 'ATHENA_BACKUP_CHECK_OK %s\n' "$ARCHIVE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen."
|
||||||
command -v docker >/dev/null || die "Docker fehlt. Zuerst ./install.sh ausführen."
|
command -v docker >/dev/null || die "Docker fehlt. Zuerst ./install.sh ausführen."
|
||||||
|
|
||||||
work=$(mktemp -d /tmp/athena-restore.XXXXXX)
|
work=$(mktemp -d /tmp/athena-restore.XXXXXX)
|
||||||
trap 'rm -rf "$work"' EXIT
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
|
||||||
# Refuse absolute paths and parent traversal before extracting as root.
|
# GNU tar removes the leading slash and extracts the single validated tree as
|
||||||
if tar -tzf "$ARCHIVE" | grep -Eq '(^/|(^|/)\.\.(/|$))'; then
|
# $work/backup. Keeping that root explicit avoids platform-dependent
|
||||||
die "Unsichere Pfade im Backup."
|
# --strip-components behaviour between GNU tar and bsdtar.
|
||||||
fi
|
|
||||||
tar -xzf "$ARCHIVE" -C "$work"
|
tar -xzf "$ARCHIVE" -C "$work"
|
||||||
[[ -d $work/etc-mike-ai ]] || die "Backup enthält etc-mike-ai nicht."
|
backup_root=$work/backup
|
||||||
[[ -d $work/volumes ]] || die "Backup enthält keine Docker-Volumes."
|
[[ -d $backup_root/etc-mike-ai ]] || die "Backup enthält etc-mike-ai nicht."
|
||||||
|
[[ -d $backup_root/volumes ]] || die "Backup enthält keine Docker-Volumes."
|
||||||
|
|
||||||
# Stop only users of the restored volumes. WireGuard, SSH and networking stay up.
|
# Stop only users of the restored volumes. WireGuard, SSH and networking stay up.
|
||||||
for container in mike-ai-router mike-ai-profile-controller mike-ai-piper; do
|
for container in mike-ai-router mike-ai-profile-controller mike-ai-piper; do
|
||||||
@@ -30,7 +64,7 @@ for container in mike-ai-router mike-ai-profile-controller mike-ai-piper; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
install -d -m 0700 /etc/mike-ai
|
install -d -m 0700 /etc/mike-ai
|
||||||
rsync -a --delete "$work/etc-mike-ai/" /etc/mike-ai/
|
rsync -a --delete "$backup_root/etc-mike-ai/" /etc/mike-ai/
|
||||||
|
|
||||||
restore_volume() {
|
restore_volume() {
|
||||||
local volume=$1 source=$2 mountpoint
|
local volume=$1 source=$2 mountpoint
|
||||||
@@ -41,9 +75,9 @@ restore_volume() {
|
|||||||
rsync -a --delete "$source/" "$mountpoint/"
|
rsync -a --delete "$source/" "$mountpoint/"
|
||||||
}
|
}
|
||||||
|
|
||||||
restore_volume mike-ai_piper-data "$work/volumes/piper-data"
|
restore_volume mike-ai_piper-data "$backup_root/volumes/piper-data"
|
||||||
restore_volume mike-ai_router-state "$work/volumes/router-state"
|
restore_volume mike-ai_router-state "$backup_root/volumes/router-state"
|
||||||
restore_volume mike-ai_router-images "$work/volumes/router-images"
|
restore_volume mike-ai_router-images "$backup_root/volumes/router-images"
|
||||||
|
|
||||||
cd "$ROOT_DIR"
|
cd "$ROOT_DIR"
|
||||||
./manage.sh deploy core
|
./manage.sh deploy core
|
||||||
|
|||||||
+2
-1
@@ -83,6 +83,7 @@ pass "Router-Liveness und OpenAI-Modellliste funktionieren"
|
|||||||
|
|
||||||
latest=/data/docker-backups/athena-latest.tar.gz
|
latest=/data/docker-backups/athena-latest.tar.gz
|
||||||
[[ -s $latest ]] || fail "Kein gueltiges Athena-Backup: $latest"
|
[[ -s $latest ]] || fail "Kein gueltiges Athena-Backup: $latest"
|
||||||
pass "Automatisches Athena-Backup ist vorhanden"
|
./restore.sh --check "$latest" >/dev/null || fail "Athena-Backup ist nicht wiederherstellbar"
|
||||||
|
pass "Automatisches Athena-Backup ist vorhanden und strukturell wiederherstellbar"
|
||||||
|
|
||||||
printf '\nATHENA_E2E_OK\n'
|
printf '\nATHENA_E2E_OK\n'
|
||||||
|
|||||||
Reference in New Issue
Block a user