567 lines
24 KiB
Bash
Executable File
567 lines
24 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Reproducible bootstrap for a fresh Debian 12/13 AI host.
|
|
set -Eeuo pipefail
|
|
umask 077
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
CONFIG=""
|
|
STACK_DIR=/opt/mike-ai/stack
|
|
SECRETS_DIR=/etc/mike-ai
|
|
STATE_DIR=/srv/mike-ai
|
|
|
|
log() { printf '\n==> %s\n' "$*"; }
|
|
die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; }
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Aufruf: sudo ./install.sh --config /root/mike-ai-install.env
|
|
|
|
Das Skript ist idempotent und darf nach einem Treiber-Reboot erneut ausgeführt
|
|
werden. Es formatiert keine Datenträger und löscht keine Modelle oder Volumes.
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--config) CONFIG="${2:-}"; shift 2 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) die "Unbekanntes Argument: $1" ;;
|
|
esac
|
|
done
|
|
|
|
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen."
|
|
[[ -n "$CONFIG" && -f "$CONFIG" ]] || die "Konfigurationsdatei fehlt (--config)."
|
|
config_mode=$(stat -c '%a' "$CONFIG")
|
|
if (( (8#$config_mode & 077) != 0 )); then
|
|
die "Konfigurationsdatei darf nicht für Gruppe/Andere schreib- oder lesbar sein (chmod 600)."
|
|
fi
|
|
# shellcheck disable=SC1090
|
|
source "$CONFIG"
|
|
|
|
required=(AI_HOSTNAME ADMIN_USER MODEL_DIR FAST_MODEL_FILE
|
|
FAST_MODEL_URL FAST_MODEL_SHA256 MEDIUM_MODEL_FILE MEDIUM_MODEL_URL
|
|
MEDIUM_MODEL_SHA256 LARGE_MODEL_FILE LARGE_MODEL_URL LARGE_MODEL_SHA256
|
|
ULTRA_MODEL_FILE ULTRA_MODEL_URL ULTRA_MODEL_SHA256
|
|
EXPERIMENTAL_MODEL_FILE EXPERIMENTAL_MODEL_URL EXPERIMENTAL_MODEL_SHA256
|
|
VISION_PROJECTOR_FILE VISION_PROJECTOR_URL VISION_PROJECTOR_SHA256)
|
|
for name in "${required[@]}"; do
|
|
[[ -n "${!name:-}" ]] || die "Pflichtwert $name fehlt."
|
|
done
|
|
if [[ ${WIREGUARD_MODE:-container} != container ]]; then
|
|
[[ -n ${AI_BIND_ADDRESS:-} ]] || die "Pflichtwert AI_BIND_ADDRESS fehlt."
|
|
fi
|
|
|
|
source /etc/os-release
|
|
[[ ${ID:-} == debian ]] || die "Unterstützt wird Debian, gefunden: ${ID:-unbekannt}."
|
|
[[ ${VERSION_ID%%.*} == 12 || ${VERSION_ID%%.*} == 13 ]] || \
|
|
die "Unterstützt werden Debian 12 und 13."
|
|
[[ $(dpkg --print-architecture) == amd64 ]] || die "Dieser Stack erwartet amd64."
|
|
id "$ADMIN_USER" >/dev/null 2>&1 || die "ADMIN_USER $ADMIN_USER existiert nicht."
|
|
|
|
install_base_packages() {
|
|
log "Basispakete installieren"
|
|
# Heal keyrings created by an older installer run under this script's 0077
|
|
# umask before apt attempts to refresh any already configured repositories.
|
|
[[ ! -e /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg ]] || \
|
|
chmod 0644 /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
|
|
apt-get update
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
ca-certificates curl git gnupg jq openssl wireguard-tools iptables \
|
|
iproute2 pciutils rsync unattended-upgrades ethtool age
|
|
}
|
|
|
|
setup_stable_network_name() {
|
|
local mac=${PRIMARY_NETWORK_MAC:-}
|
|
local desired=${PERSISTENT_NETWORK_NAME:-}
|
|
[[ -n $mac && -n $desired ]] || return 0
|
|
[[ $mac =~ ^([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}$ ]] || \
|
|
die "PRIMARY_NETWORK_MAC ist ungültig: $mac"
|
|
[[ $desired =~ ^[a-zA-Z0-9_.-]+$ ]] || \
|
|
die "PERSISTENT_NETWORK_NAME ist ungültig: $desired"
|
|
|
|
mac=${mac,,}
|
|
local current="" path
|
|
for path in /sys/class/net/*; do
|
|
[[ -f $path/address ]] || continue
|
|
if [[ $(<"$path/address") == "$mac" ]]; then
|
|
current=${path##*/}
|
|
break
|
|
fi
|
|
done
|
|
[[ -n $current ]] || die "Keine Netzwerkkarte mit permanenter MAC $mac gefunden."
|
|
|
|
log "Stabilen Netzwerknamen $desired für $mac konfigurieren"
|
|
install -d -m 0755 /etc/systemd/network
|
|
cat >/etc/systemd/network/10-athena-lan.link <<EOF
|
|
[Match]
|
|
PermanentMACAddress=$mac
|
|
|
|
[Link]
|
|
Name=$desired
|
|
EOF
|
|
chmod 0644 /etc/systemd/network/10-athena-lan.link
|
|
|
|
if [[ $current != "$desired" ]]; then
|
|
[[ -f /etc/network/interfaces ]] || \
|
|
die "/etc/network/interfaces fehlt; sichere automatische Umstellung nicht möglich."
|
|
cp -a /etc/network/interfaces \
|
|
"/etc/network/interfaces.before-stable-name-$(date +%Y%m%d-%H%M%S)"
|
|
sed -i "s/\\<$current\\>/$desired/g" /etc/network/interfaces
|
|
grep -q "^iface $desired inet " /etc/network/interfaces || \
|
|
die "Netzwerkprofil wurde nicht auf $desired umgestellt."
|
|
log "Netzwerkname wird beim nächsten Boot von $current auf $desired geändert."
|
|
log "Bitte neu starten und denselben Installer danach erneut ausführen."
|
|
exit 21
|
|
fi
|
|
}
|
|
|
|
setup_remote_recovery() {
|
|
log "Remote-Recovery (Hardware-Watchdog und Wake-on-LAN) konfigurieren"
|
|
|
|
if [[ -n ${PRIMARY_NETWORK_INTERFACE:-} ]]; then
|
|
ip link show "$PRIMARY_NETWORK_INTERFACE" >/dev/null 2>&1 || \
|
|
die "Primäres Netzwerk-Interface existiert nicht: $PRIMARY_NETWORK_INTERFACE"
|
|
if [[ -f /etc/network/interfaces ]] && \
|
|
grep -q "^allow-hotplug $PRIMARY_NETWORK_INTERFACE\$" /etc/network/interfaces && \
|
|
! grep -q "^auto $PRIMARY_NETWORK_INTERFACE\$" /etc/network/interfaces; then
|
|
sed -i "/^allow-hotplug $PRIMARY_NETWORK_INTERFACE\$/i auto $PRIMARY_NETWORK_INTERFACE" \
|
|
/etc/network/interfaces
|
|
fi
|
|
fi
|
|
|
|
if [[ ${ENABLE_HARDWARE_WATCHDOG:-true} == true ]]; then
|
|
[[ -e /dev/watchdog0 || -e /dev/watchdog ]] || \
|
|
die "Hardware-Watchdog angefordert, aber kein Watchdog-Gerät vorhanden."
|
|
install -d -m 0755 /etc/systemd/system.conf.d
|
|
cat >/etc/systemd/system.conf.d/90-mike-ai-watchdog.conf <<'EOF'
|
|
[Manager]
|
|
# PID 1 feeds the hardware watchdog. If the kernel or userspace freezes long
|
|
# enough that it cannot be fed, the firmware resets the machine.
|
|
RuntimeWatchdogSec=60s
|
|
RebootWatchdogSec=10min
|
|
KExecWatchdogSec=10min
|
|
EOF
|
|
systemctl daemon-reexec
|
|
fi
|
|
|
|
if [[ -n ${WAKE_ON_LAN_INTERFACE:-} ]]; then
|
|
ip link show "$WAKE_ON_LAN_INTERFACE" >/dev/null 2>&1 || \
|
|
die "Wake-on-LAN-Interface existiert nicht: $WAKE_ON_LAN_INTERFACE"
|
|
ethtool "$WAKE_ON_LAN_INTERFACE" | grep -q 'Supports Wake-on:.*g' || \
|
|
die "Das Interface unterstützt kein Wake-on-LAN per Magic Packet."
|
|
cat >/etc/network/if-up.d/mike-ai-wol <<EOF
|
|
#!/bin/sh
|
|
[ "\${IFACE:-}" = "$WAKE_ON_LAN_INTERFACE" ] || exit 0
|
|
/usr/sbin/ethtool -s "$WAKE_ON_LAN_INTERFACE" wol g
|
|
EOF
|
|
chmod 0755 /etc/network/if-up.d/mike-ai-wol
|
|
ethtool -s "$WAKE_ON_LAN_INTERFACE" wol g
|
|
fi
|
|
}
|
|
|
|
setup_ssh_hardening() {
|
|
[[ ${SSH_KEY_ONLY:-true} == true ]] || return 0
|
|
log "SSH auf Schlüsselanmeldung beschränken"
|
|
install -d -m 0755 /etc/ssh/sshd_config.d
|
|
cat >/etc/ssh/sshd_config.d/20-athena-key-only.conf <<'EOF'
|
|
PasswordAuthentication no
|
|
KbdInteractiveAuthentication no
|
|
PubkeyAuthentication yes
|
|
EOF
|
|
sshd -t
|
|
systemctl reload ssh
|
|
}
|
|
|
|
install_docker() {
|
|
log "Docker CE aus dem offiziellen Repository installieren"
|
|
install -m 0755 -d /etc/apt/keyrings
|
|
curl -fsSL https://download.docker.com/linux/debian/gpg \
|
|
-o /etc/apt/keyrings/docker.asc
|
|
chmod a+r /etc/apt/keyrings/docker.asc
|
|
cat >/etc/apt/sources.list.d/docker.sources <<EOF
|
|
Types: deb
|
|
URIs: https://download.docker.com/linux/debian
|
|
Suites: ${VERSION_CODENAME}
|
|
Components: stable
|
|
Architectures: $(dpkg --print-architecture)
|
|
Signed-By: /etc/apt/keyrings/docker.asc
|
|
EOF
|
|
apt-get update
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
|
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
|
systemctl enable --now docker
|
|
}
|
|
|
|
install_nvidia() {
|
|
if ! command -v nvidia-smi >/dev/null 2>&1; then
|
|
[[ ${INSTALL_NVIDIA_DRIVER:-false} == true ]] || \
|
|
die "NVIDIA-Treiber fehlt. Installiere ihn oder setze INSTALL_NVIDIA_DRIVER=true."
|
|
log "Aktuellen NVIDIA-Treiber aus dem offiziellen NVIDIA-Repository installieren"
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
"linux-headers-$(uname -r)"
|
|
|
|
local distro="debian${VERSION_ID%%.*}"
|
|
local keyring_url="https://developer.download.nvidia.com/compute/cuda/repos/${distro}/x86_64/cuda-keyring_1.1-1_all.deb"
|
|
local keyring_deb
|
|
keyring_deb=$(mktemp /tmp/cuda-keyring.XXXXXX.deb)
|
|
curl --fail --location --retry 5 --output "$keyring_deb" "$keyring_url"
|
|
dpkg -i "$keyring_deb"
|
|
rm -f "$keyring_deb"
|
|
apt-get update
|
|
|
|
if [[ -n ${NVIDIA_DRIVER_BRANCH:-} ]]; then
|
|
local pinning="nvidia-driver-pinning-${NVIDIA_DRIVER_BRANCH}"
|
|
apt-cache show "$pinning" >/dev/null 2>&1 || \
|
|
die "Gewuenschter NVIDIA-Treiberzweig fehlt im offiziellen Repository: $pinning"
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y "$pinning"
|
|
fi
|
|
|
|
# RTX 30/50 werden beide von NVIDIAs offenen Kernelmodulen unterstuetzt.
|
|
# Compute-only vermeidet X11-, Desktop- und Vulkan-Pakete auf dem Headless-Host.
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
|
nvidia-driver-cuda nvidia-kernel-open-dkms
|
|
printf '\nNVIDIA-Treiber installiert. Jetzt neu starten und danach denselben Installer erneut ausführen.\n'
|
|
exit 20
|
|
fi
|
|
nvidia-smi >/dev/null || die "nvidia-smi ist vorhanden, aber der Treiber funktioniert nicht."
|
|
|
|
local driver_major min_driver_major
|
|
driver_major=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader | head -n1 | cut -d. -f1)
|
|
min_driver_major=${NVIDIA_MIN_DRIVER_MAJOR:-570}
|
|
[[ $driver_major =~ ^[0-9]+$ ]] || die "NVIDIA-Treiberversion konnte nicht gelesen werden."
|
|
(( driver_major >= min_driver_major )) || \
|
|
die "NVIDIA-Treiber $driver_major ist zu alt; mindestens $min_driver_major wird verlangt."
|
|
|
|
log "NVIDIA Container Toolkit installieren"
|
|
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
|
|
gpg --dearmor --yes -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
|
|
# apt verifies repository metadata as the unprivileged _apt user. The
|
|
# installer's restrictive umask would otherwise leave this keyring at 0600.
|
|
chmod 0644 /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
|
|
curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
|
|
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
|
|
>/etc/apt/sources.list.d/nvidia-container-toolkit.list
|
|
apt-get update
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y nvidia-container-toolkit
|
|
nvidia-ctk runtime configure --runtime=docker
|
|
systemctl restart docker
|
|
docker run --rm --gpus all nvidia/cuda:12.8.1-base-ubuntu24.04 nvidia-smi >/dev/null
|
|
}
|
|
|
|
setup_wireguard() {
|
|
[[ ${WIREGUARD_MODE:-container} != container ]] || return 0
|
|
[[ ${WIREGUARD_ENABLE:-false} == true ]] || return 0
|
|
for name in WG_INTERFACE WG_ADDRESS WG_HOME_SUBNET WG_PEER_PUBLIC_KEY WG_PEER_ENDPOINT; do
|
|
[[ -n "${!name:-}" && ${!name} != REPLACE_* ]] || die "WireGuard-Wert $name fehlt."
|
|
done
|
|
log "WireGuard als ausgehenden Heimnetz-Tunnel konfigurieren"
|
|
install -d -m 0700 /etc/wireguard
|
|
local key_file="/etc/wireguard/${WG_INTERFACE}.key"
|
|
[[ -s $key_file ]] || wg genkey >"$key_file"
|
|
chmod 0600 "$key_file"
|
|
local private_key
|
|
private_key=$(<"$key_file")
|
|
local allowed_ips="$WG_HOME_SUBNET"
|
|
[[ ${WG_ROUTE_AI_INTERNET:-true} == true ]] && allowed_ips="0.0.0.0/0, ::/0"
|
|
{
|
|
printf '[Interface]\nAddress = %s\nPrivateKey = %s\nTable = off\n' "$WG_ADDRESS" "$private_key"
|
|
printf '\n[Peer]\nPublicKey = %s\nEndpoint = %s\nAllowedIPs = %s\nPersistentKeepalive = 25\n' \
|
|
"$WG_PEER_PUBLIC_KEY" "$WG_PEER_ENDPOINT" "$allowed_ips"
|
|
if [[ -n ${WG_PEER_PRESHARED_KEY_FILE:-} ]]; then
|
|
[[ -s $WG_PEER_PRESHARED_KEY_FILE ]] || die "WireGuard-Preshared-Key-Datei fehlt."
|
|
printf 'PresharedKey = %s\n' "$(<"$WG_PEER_PRESHARED_KEY_FILE")"
|
|
fi
|
|
} >"/etc/wireguard/${WG_INTERFACE}.conf"
|
|
chmod 0600 "/etc/wireguard/${WG_INTERFACE}.conf"
|
|
systemctl enable --now "wg-quick@${WG_INTERFACE}"
|
|
ip address show "$WG_INTERFACE" >/dev/null
|
|
local wg_host=${WG_ADDRESS%/*}
|
|
[[ $AI_BIND_ADDRESS == "$wg_host" ]] || \
|
|
die "AI_BIND_ADDRESS muss der WireGuard-Adresse ohne Präfix entsprechen ($wg_host)."
|
|
printf 'WireGuard-Public-Key des KI-Hosts: %s\n' "$(wg pubkey <"$key_file")"
|
|
}
|
|
|
|
install_stack_files() {
|
|
log "Stackdateien installieren"
|
|
XTTS_CACHE_DIR=${XTTS_CACHE_DIR:-$MODEL_DIR/xtts-v2-cache}
|
|
install -d -m 0755 "$STACK_DIR" "$MODEL_DIR" "$XTTS_CACHE_DIR" "$STATE_DIR/backups"
|
|
rsync -a --delete --exclude .git --exclude '*.local.*' \
|
|
--exclude config/install.env "$ROOT_DIR/" "$STACK_DIR/"
|
|
if git -C "$ROOT_DIR" rev-parse HEAD >/dev/null 2>&1; then
|
|
git -C "$ROOT_DIR" rev-parse HEAD >"$STACK_DIR/.mike-ai-source-commit"
|
|
fi
|
|
install -d -m 0700 "$SECRETS_DIR"
|
|
[[ -s $SECRETS_DIR/router-api-key ]] || openssl rand -base64 48 >$SECRETS_DIR/router-api-key
|
|
[[ -s $SECRETS_DIR/controller-token ]] || openssl rand -base64 48 >$SECRETS_DIR/controller-token
|
|
[[ -s $SECRETS_DIR/webui-secret ]] || openssl rand -base64 48 >$SECRETS_DIR/webui-secret
|
|
chmod 0600 "$SECRETS_DIR"/*
|
|
|
|
local searx="$STACK_DIR/platform/web-search/searxng-settings.yml"
|
|
if [[ ! -s $searx ]]; then
|
|
cp "$STACK_DIR/platform/web-search/searxng-settings.example.yml" "$searx"
|
|
sed -i "s/CHANGE_ME_GENERATE_RANDOM_SECRET/$(openssl rand -hex 32)/" "$searx"
|
|
fi
|
|
# The official image reads this as its unprivileged uid (977).
|
|
chown root:977 "$searx"
|
|
chmod 0640 "$searx"
|
|
|
|
cat >$SECRETS_DIR/stack.env <<EOF
|
|
AI_BIND_ADDRESS=${AI_BIND_ADDRESS:-127.0.0.1}
|
|
MODEL_DIR=$MODEL_DIR
|
|
WIREGUARD_CONFIG_FILE=${WIREGUARD_CONFIG_FILE:-/etc/mike-ai/wireguard/fritz-athena.conf}
|
|
ROUTER_API_KEY=$(<$SECRETS_DIR/router-api-key)
|
|
CONTROLLER_TOKEN=$(<$SECRETS_DIR/controller-token)
|
|
WEBUI_SECRET_KEY=$(<$SECRETS_DIR/webui-secret)
|
|
OPENWEBUI_IMAGE=${OPENWEBUI_IMAGE:-mike-ai/openwebui:main-01f4282-tool-final-v1}
|
|
OPENWEBUI_ENABLE_SIGNUP=${OPENWEBUI_ENABLE_SIGNUP:-false}
|
|
OPENWEBUI_ENABLE_FOLLOW_UP_GENERATION=${OPENWEBUI_ENABLE_FOLLOW_UP_GENERATION:-false}
|
|
PIPER_TTS_VERSION=${PIPER_TTS_VERSION:-1.6.0}
|
|
PIPER_VOICE=${PIPER_VOICE:-de_DE-thorsten-high}
|
|
XTTS_IMAGE=${XTTS_IMAGE:-ghcr.io/coqui-ai/xtts-streaming-server:latest-cuda121@sha256:f7fb3b1f9d4bc88af94da1b5959d8002f1e0b003c97557164034eb8a29f01b90}
|
|
XTTS_CACHE_DIR=$XTTS_CACHE_DIR
|
|
XTTS_GPU_DEVICE=${XTTS_GPU_DEVICE:-GPU-4834d9d7-5b61-3004-1fb3-4ae49d482d4b}
|
|
AI_DNS=${WG_DNS:-1.1.1.1}
|
|
FAST_MODEL_FILE=$FAST_MODEL_FILE
|
|
MEDIUM_MODEL_FILE=$MEDIUM_MODEL_FILE
|
|
LARGE_MODEL_FILE=$LARGE_MODEL_FILE
|
|
ULTRA_MODEL_FILE=$ULTRA_MODEL_FILE
|
|
UNCENSORED_MODEL_FILE=$UNCENSORED_MODEL_FILE
|
|
UNCENSORED_PROJECTOR_FILE=$UNCENSORED_PROJECTOR_FILE
|
|
EXPERIMENTAL_MODEL_FILE=$EXPERIMENTAL_MODEL_FILE
|
|
VISION_PROJECTOR_FILE=$VISION_PROJECTOR_FILE
|
|
FAST_CONTEXT=${FAST_CONTEXT:-76800}
|
|
MEDIUM_CONTEXT=${MEDIUM_CONTEXT:-160000}
|
|
LARGE_CONTEXT=${LARGE_CONTEXT:-192000}
|
|
ULTRA_CONTEXT=${ULTRA_CONTEXT:-262144}
|
|
UNCENSORED_CONTEXT=${UNCENSORED_CONTEXT:-80000}
|
|
EXPERIMENTAL_CONTEXT=${EXPERIMENTAL_CONTEXT:-76800}
|
|
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_TENSOR_SPLIT=${MEDIUM_TENSOR_SPLIT:-90,10}
|
|
LARGE_GPU_DEVICES=${TEXT_GPU_DEVICES:-0}${SECONDARY_GPU_DEVICES:+,$SECONDARY_GPU_DEVICES}
|
|
LARGE_TENSOR_SPLIT=${LARGE_TENSOR_SPLIT:-86,14}
|
|
ULTRA_GPU_DEVICES=${TEXT_GPU_DEVICES:-0}${SECONDARY_GPU_DEVICES:+,$SECONDARY_GPU_DEVICES}
|
|
ULTRA_TENSOR_SPLIT=${ULTRA_TENSOR_SPLIT:-80,20}
|
|
UNCENSORED_GPU_DEVICES=${TEXT_GPU_DEVICES:-0}${SECONDARY_GPU_DEVICES:+,$SECONDARY_GPU_DEVICES}
|
|
UNCENSORED_TENSOR_SPLIT=${UNCENSORED_TENSOR_SPLIT:-90,10}
|
|
UNCENSORED_MTP_MAX=${UNCENSORED_MTP_MAX:-2}
|
|
EXPERIMENTAL_GPU_DEVICES=${TEXT_GPU_DEVICES:-0}
|
|
IMAGE_GPU_DEVICES=${IMAGE_GPU_DEVICES:-${TEXT_GPU_DEVICES:-0}}
|
|
FLUX_MODEL_DIR=${FLUX_MODEL_DIR:-/data/models/FLUX.2-klein-4B}
|
|
LLAMA_THREADS=${LLAMA_THREADS:-6}
|
|
LLAMA_THREADS_BATCH=${LLAMA_THREADS_BATCH:-6}
|
|
EOF
|
|
chmod 0600 $SECRETS_DIR/stack.env
|
|
}
|
|
|
|
download_one() {
|
|
local relative=$1 url=$2 expected=$3 target="$MODEL_DIR/$1"
|
|
install -d -m 0755 "$(dirname "$target")"
|
|
if [[ -f $target ]] && printf '%s %s\n' "$expected" "$target" | sha256sum -c - >/dev/null 2>&1; then
|
|
# The installer itself runs with umask 077, but inference runs deliberately
|
|
# unprivileged. Models are immutable inputs: globally readable, never
|
|
# writable by the runtime container.
|
|
chmod 0444 "$target"
|
|
printf 'Vorhanden und geprüft: %s\n' "$relative"
|
|
return
|
|
fi
|
|
log "Modell laden: $relative"
|
|
curl --fail --location --continue-at - --retry 5 --retry-all-errors \
|
|
--output "$target.partial" "$url"
|
|
printf '%s %s\n' "$expected" "$target.partial" | sha256sum -c -
|
|
mv "$target.partial" "$target"
|
|
chmod 0444 "$target"
|
|
}
|
|
|
|
download_models() {
|
|
[[ ${SKIP_MODEL_DOWNLOADS:-false} == true ]] && return 0
|
|
declare -A seen=()
|
|
local row relative url hash
|
|
while IFS='|' read -r relative url hash; do
|
|
[[ -n ${seen[$relative]:-} ]] && continue
|
|
seen[$relative]=1
|
|
download_one "$relative" "$url" "$hash"
|
|
done <<EOF
|
|
$FAST_MODEL_FILE|$FAST_MODEL_URL|$FAST_MODEL_SHA256
|
|
$MEDIUM_MODEL_FILE|$MEDIUM_MODEL_URL|$MEDIUM_MODEL_SHA256
|
|
$LARGE_MODEL_FILE|$LARGE_MODEL_URL|$LARGE_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
|
|
$EXPERIMENTAL_MODEL_FILE|$EXPERIMENTAL_MODEL_URL|$EXPERIMENTAL_MODEL_SHA256
|
|
$VISION_PROJECTOR_FILE|$VISION_PROJECTOR_URL|$VISION_PROJECTOR_SHA256
|
|
EOF
|
|
}
|
|
|
|
install_routing_guard() {
|
|
if [[ ${WIREGUARD_MODE:-container} == container ]]; then
|
|
log "Container-WireGuard-Routing installieren"
|
|
[[ -s ${WIREGUARD_CONFIG_FILE:-/etc/mike-ai/wireguard/fritz-athena.conf} ]] || \
|
|
die "Fritzbox-WireGuard-Datei fehlt: ${WIREGUARD_CONFIG_FILE:-/etc/mike-ai/wireguard/fritz-athena.conf}"
|
|
install -m 0755 "$ROOT_DIR/platform/host/mike-ai-container-vpn-guard" \
|
|
/usr/local/sbin/mike-ai-container-vpn-guard
|
|
install -m 0644 "$ROOT_DIR/platform/host/mike-ai-container-vpn-guard.service" \
|
|
/etc/systemd/system/mike-ai-container-vpn-guard.service
|
|
install -m 0755 "$ROOT_DIR/platform/host/mike-ai-emergency-access" \
|
|
/usr/local/sbin/mike-ai-emergency-access
|
|
systemctl daemon-reload
|
|
systemctl enable mike-ai-container-vpn-guard.service
|
|
return 0
|
|
fi
|
|
[[ ${WIREGUARD_ENABLE:-false} == true ]] || return 0
|
|
log "KI-Container auf WireGuard routen und Uni-Netz als Transit sperren"
|
|
cat >/usr/local/sbin/mike-ai-network-guard <<EOF
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
WG=$WG_INTERFACE
|
|
HOME_NET=$WG_HOME_SUBNET
|
|
TABLE=51820
|
|
for NET in 172.30.10.0/24 172.30.30.0/24 172.30.40.0/24 172.30.50.0/24; do
|
|
ip rule add from \"\$NET\" table \"\$TABLE\" priority 12000 2>/dev/null || true
|
|
done
|
|
ip route replace \"\$HOME_NET\" dev \"\$WG\"
|
|
ip route replace \"\$HOME_NET\" dev \"\$WG\" table \"\$TABLE\"
|
|
ip route replace blackhole default metric 32767 table \"\$TABLE\"
|
|
EOF
|
|
if [[ ${WG_ROUTE_AI_INTERNET:-true} == true ]]; then
|
|
printf 'ip route replace default dev "$WG" metric 10 table "$TABLE"\n' >>/usr/local/sbin/mike-ai-network-guard
|
|
fi
|
|
cat >>/usr/local/sbin/mike-ai-network-guard <<'EOF'
|
|
# Explicit forwarding policy: AI containers may use wg0; WireGuard clients may
|
|
# reach the two published UI/API ports. Neither side may use this host as a
|
|
# general university<->home router. The blackhole route above prevents a
|
|
# fail-open to the university gateway when wg0 loses its peer/default route.
|
|
add_rule() { iptables -C DOCKER-USER "$@" 2>/dev/null || iptables -I DOCKER-USER 1 "$@"; }
|
|
add_rule -o "$WG" -j DROP
|
|
add_rule -i "$WG" -j DROP
|
|
add_rule -s 172.30.0.0/16 -o "$WG" -j ACCEPT
|
|
add_rule -i "$WG" -d 172.30.10.0/24 -p tcp -m multiport --dports 8080,8081 -j ACCEPT
|
|
add_rule -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
|
EOF
|
|
chmod 0755 /usr/local/sbin/mike-ai-network-guard
|
|
cat >/etc/systemd/system/mike-ai-network-guard.service <<EOF
|
|
[Unit]
|
|
Description=Routing guard for Mike AI Docker networks
|
|
After=docker.service wg-quick@${WG_INTERFACE}.service
|
|
Requires=docker.service wg-quick@${WG_INTERFACE}.service
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/usr/local/sbin/mike-ai-network-guard
|
|
RemainAfterExit=yes
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
systemctl daemon-reload
|
|
systemctl enable --now mike-ai-network-guard.service
|
|
}
|
|
|
|
build_and_start() {
|
|
log "llama.cpp und Plattform-Container bauen"
|
|
local commit
|
|
commit=$(<"$STACK_DIR/platform/llama/LLAMA_CPP_COMMIT")
|
|
cd "$STACK_DIR"
|
|
docker build --progress=plain --build-arg LLAMA_CPP_COMMIT="$commit" \
|
|
-f platform/docker/llama-cpp/Dockerfile -t mike-ai/llama.cpp:local .
|
|
docker compose --env-file "$SECRETS_DIR/stack.env" --profile image build flux-worker
|
|
if [[ ! -s ${FLUX_MODEL_DIR:-/data/models/FLUX.2-klein-4B}/model_index.json ]]; then
|
|
log "FLUX.2 Klein Distilled laden"
|
|
install -d -m 0755 "${FLUX_MODEL_DIR:-/data/models/FLUX.2-klein-4B}"
|
|
docker run --rm --entrypoint python \
|
|
-v "${FLUX_MODEL_DIR:-/data/models/FLUX.2-klein-4B}:/download" \
|
|
mike-ai/flux-worker:local -c \
|
|
"from huggingface_hub import snapshot_download; snapshot_download('black-forest-labs/FLUX.2-klein-4B', revision='303481f0390afb112393f9d77e8f0be72fcefeb7', local_dir='/download')"
|
|
chmod -R a-w "${FLUX_MODEL_DIR:-/data/models/FLUX.2-klein-4B}"
|
|
fi
|
|
# Creates the shared internal tools network before Open WebUI is created.
|
|
# Web search always starts; HA/ARR/Unraid only start when their root-only
|
|
# secret files and required local artifacts are present.
|
|
"$STACK_DIR/platform/mcp/install-tools.sh"
|
|
docker compose --env-file "$SECRETS_DIR/stack.env" --profile inference create \
|
|
llama-fast llama-medium llama-large llama-ultra llama-experimental
|
|
docker compose --env-file "$SECRETS_DIR/stack.env" --profile image create flux-worker
|
|
docker compose --env-file "$SECRETS_DIR/stack.env" up -d --build \
|
|
xtts piper tts-gateway profile-controller router open-webui
|
|
|
|
if [[ ${WIREGUARD_MODE:-container} == container ]]; then
|
|
systemctl restart mike-ai-container-vpn-guard.service
|
|
fi
|
|
|
|
log "Auf gesunden Router warten"
|
|
local deadline=$((SECONDS + 180))
|
|
until [ "$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' mike-ai-router 2>/dev/null || true)" = "healthy" ]; do
|
|
if (( SECONDS >= deadline )); then
|
|
docker logs --tail 80 mike-ai-router >&2 || true
|
|
die "Router wurde nicht rechtzeitig gesund"
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
log "Medium-Profil als Standard aktivieren und Readiness prüfen"
|
|
# Use docker exec directly here. Some Compose/Docker combinations return a
|
|
# transient HTTP 409 while upgrading the exec stream immediately after a
|
|
# freshly built service has been recreated.
|
|
local activation_output
|
|
activation_output=$(docker exec -i mike-ai-router python - <<'PY'
|
|
import json, os, time, urllib.request
|
|
key = os.environ["ROUTER_API_KEY"]
|
|
request = urllib.request.Request(
|
|
"http://127.0.0.1:8081/medium", method="POST",
|
|
headers={"Authorization": f"Bearer {key}"})
|
|
with urllib.request.urlopen(request, timeout=700) as response:
|
|
print(json.dumps(json.load(response), indent=2))
|
|
deadline = time.monotonic() + 60
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
with urllib.request.urlopen("http://127.0.0.1:8081/ready", timeout=5) as response:
|
|
if response.status == 200:
|
|
print("Router und Medium-Standardprofil sind bereit.")
|
|
print("INSTALL_READINESS_OK")
|
|
break
|
|
except Exception:
|
|
pass
|
|
time.sleep(2)
|
|
else:
|
|
raise SystemExit("Readiness-Check fehlgeschlagen")
|
|
PY
|
|
) || die "Medium-Standardprofil konnte nicht aktiviert werden"
|
|
printf '%s\n' "$activation_output"
|
|
grep -Fxq 'INSTALL_READINESS_OK' <<<"$activation_output" || \
|
|
die "Medium-Standardprofil lieferte keinen bestätigten Readiness-Marker"
|
|
}
|
|
|
|
hostnamectl set-hostname "$AI_HOSTNAME"
|
|
install_base_packages
|
|
setup_stable_network_name
|
|
setup_remote_recovery
|
|
setup_ssh_hardening
|
|
install_docker
|
|
install_nvidia
|
|
setup_wireguard
|
|
install_stack_files
|
|
download_models
|
|
install_routing_guard
|
|
build_and_start
|
|
|
|
log "Installation abgeschlossen"
|
|
if [[ ${WIREGUARD_MODE:-container} == container ]]; then
|
|
vpn_address=$(awk -F= '
|
|
/^[[:space:]]*Address[[:space:]]*=/ {
|
|
value=$2; gsub(/[[:space:]]/, "", value); split(value, addresses, ",")
|
|
for (i in addresses) if (addresses[i] !~ /:/) { sub(/\/.*/, "", addresses[i]); print addresses[i]; exit }
|
|
}
|
|
' "${WIREGUARD_CONFIG_FILE:-/etc/mike-ai/wireguard/fritz-athena.conf}")
|
|
else
|
|
vpn_address=$AI_BIND_ADDRESS
|
|
fi
|
|
cat <<EOF
|
|
OpenWebUI: http://${vpn_address}:8080
|
|
Router-API: http://${vpn_address}:8081
|
|
|
|
Die geheimen Schlüssel liegen ausschließlich unter $SECRETS_DIR (0600).
|
|
Im Container-Modus stammen Peer, Adresse und Heimrouten vollständig aus dem
|
|
root-only Fritzbox-Export. Die physischen Host-Adressen veröffentlichen keine
|
|
KI-Ports.
|
|
EOF
|