Files
patrickandClaude Sonnet 5.5 15676a3600
test / pytest (3.11) (push) Canceled after 0s
test / pytest (3.13) (push) Canceled after 0s
feat: Preflight-Checks und VM-Tauglichkeit des Setups
deploy/lib/preflight.sh prüft Python, venv, Platz, Betriebssystem, Umgebung
(VM/Container), pypi und optional Checkmk/Ollama per TCP. first-run.sh und
setup-lxc.sh rufen ihn auf (--skip-preflight, --preflight-only). LXC-Teile
nur bei erkanntem Container; Texte neutral; README um VM-Abschnitt ergänzt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 15:09:53 +01:00

257 lines
9.1 KiB
Bash

#!/usr/bin/env bash
# Preflight-Prüfungen für first-run.sh und setup-lxc.sh (wird gesourced, braucht kein root, ändert nichts am System).
# Eigene Ausgabefunktionen mit Präfix pf_, damit sie unabhängig von common.sh funktionieren.
PF_LIB_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
PF_REPO_DIR=$(dirname "$(dirname "$PF_LIB_DIR")")
PF_MIN_PYTHON=${PF_MIN_PYTHON:-3.11}
PF_MIN_DISK_MB=${PF_MIN_DISK_MB:-500}
PF_NET_TIMEOUT=${PF_NET_TIMEOUT:-8}
PF_PYPI_URL=${PF_PYPI_URL:-https://pypi.org/simple/}
PF_UBUNTU_MIN_MAJOR=22
PF_PACKAGES_PENDING=${PF_PACKAGES_PENDING:-0}
PF_ERRORS=0
PF_WARNINGS=0
PF_REPORT=${PF_REPORT:-}
PF_VIRT_KIND=unbekannt
PF_VIRT_NAME=
pf_emit() {
printf '%s\n' "$1"
if [[ -n $PF_REPORT ]]; then printf '%s\n' "$1" >> "$PF_REPORT"; fi
}
pf_ok() { pf_emit "[ok] $1"; }
pf_info() { pf_emit "[info] $1"; }
pf_warn() {
PF_WARNINGS=$((PF_WARNINGS + 1))
pf_emit "[WARNUNG] $1"
pf_emit " Abhilfe: $2"
}
pf_fail() {
PF_ERRORS=$((PF_ERRORS + 1))
pf_emit "[FEHLER] $1"
pf_emit " Abhilfe: $2"
}
pf_check_python() {
local version
if ! command -v python3 >/dev/null 2>&1; then
if (( PF_PACKAGES_PENDING )); then
pf_warn "python3 ist nicht installiert." "wird vom Skript per apt installiert."
else
pf_fail "python3 ist nicht installiert." "apt-get install python3 python3-venv python3-pip"
fi
return 0
fi
version=$(python3 -c 'import sys; print("%d.%d.%d" % sys.version_info[:3])' 2>/dev/null) || version=""
if [[ -z $version ]]; then
pf_fail "python3 lässt sich nicht ausführen." "Python-Installation prüfen (python3 --version)."
return 0
fi
if python3 -c "import sys; sys.exit(0 if sys.version_info >= tuple(map(int, '$PF_MIN_PYTHON'.split('.'))) else 1)"; then
pf_ok "Python $version (mindestens $PF_MIN_PYTHON)."
else
pf_fail "Python $version ist älter als $PF_MIN_PYTHON." "Debian 12/13 oder Ubuntu 22.04+ verwenden bzw. ein neueres Python bereitstellen."
fi
if python3 -c 'import venv, ensurepip' >/dev/null 2>&1; then
pf_ok "Python-Module venv und ensurepip vorhanden."
elif (( PF_PACKAGES_PENDING )); then
pf_warn "Python-Modul venv/ensurepip fehlt." "wird mit python3-venv vom Skript installiert."
else
pf_fail "Python-Modul venv/ensurepip fehlt." "apt-get install python3-venv python3-pip"
fi
}
pf_existing_parent() {
local path=$1
while [[ ! -d $path && $path != / && -n $path ]]; do path=$(dirname "$path"); done
printf '%s' "${path:-/}"
}
pf_check_disk_dir() {
local dir existing free
dir=$1
existing=$(pf_existing_parent "$dir")
free=$(df -Pm "$existing" 2>/dev/null | awk 'NR==2 {print $4}')
if [[ ! $free =~ ^[0-9]+$ ]]; then
pf_warn "Freier Platz für $dir nicht ermittelbar." "df -h $existing manuell prüfen."
elif (( free < PF_MIN_DISK_MB )); then
pf_fail "Nur $free MB frei für $dir (benötigt: $PF_MIN_DISK_MB MB)." "Platz schaffen oder ein anderes Verzeichnis wählen."
else
pf_ok "Freier Platz für $dir: $free MB (mindestens $PF_MIN_DISK_MB MB)."
fi
}
pf_check_os() {
local id="" version="" pretty="" major
if [[ ! -r /etc/os-release ]]; then
pf_warn "/etc/os-release fehlt, Betriebssystem unbekannt." "Getestet ist nur Debian 12/13 und Ubuntu ab $PF_UBUNTU_MIN_MAJOR.04."
return 0
fi
id=$(. /etc/os-release && printf '%s' "${ID:-}")
version=$(. /etc/os-release && printf '%s' "${VERSION_ID:-}")
pretty=$(. /etc/os-release && printf '%s' "${PRETTY_NAME:-$id $version}")
major=${version%%.*}
case $id in
debian)
if [[ $version == 12 || $version == 13 ]]; then
pf_ok "Betriebssystem: $pretty."
else
pf_warn "Betriebssystem: $pretty, getestet ist nur Debian 12/13." "Auf eigene Verantwortung fortfahren oder Debian 12/13 verwenden."
fi
;;
ubuntu)
if [[ $major =~ ^[0-9]+$ ]] && (( major >= PF_UBUNTU_MIN_MAJOR )); then
pf_ok "Betriebssystem: $pretty."
else
pf_warn "Betriebssystem: $pretty, erwartet Ubuntu ab $PF_UBUNTU_MIN_MAJOR.04." "Neuere Ubuntu-Version verwenden."
fi
;;
*)
pf_warn "Betriebssystem: $pretty, getestet ist nur Debian 12/13 und Ubuntu ab $PF_UBUNTU_MIN_MAJOR.04." "Auf eigene Verantwortung fortfahren."
;;
esac
}
# Setzt PF_VIRT_KIND (container, vm, bare-metal, unbekannt) und PF_VIRT_NAME ohne Ausgabe.
pf_detect_env() {
local name=""
PF_VIRT_KIND=unbekannt
PF_VIRT_NAME=
if command -v systemd-detect-virt >/dev/null 2>&1; then
if name=$(systemd-detect-virt --container 2>/dev/null); then
PF_VIRT_KIND=container; PF_VIRT_NAME=$name; return 0
fi
if name=$(systemd-detect-virt --vm 2>/dev/null); then
PF_VIRT_KIND=vm; PF_VIRT_NAME=$name; return 0
fi
PF_VIRT_KIND=bare-metal
return 0
fi
if [[ -r /run/systemd/container ]]; then
PF_VIRT_KIND=container; PF_VIRT_NAME=$(tr -d '\n' < /run/systemd/container 2>/dev/null || true); return 0
fi
if [[ -f /.dockerenv ]] || tr '\0' '\n' < /proc/1/environ 2>/dev/null | grep -q '^container='; then
PF_VIRT_KIND=container; PF_VIRT_NAME=lxc-oder-docker; return 0
fi
if grep -qw hypervisor /proc/cpuinfo 2>/dev/null; then
PF_VIRT_KIND=vm; return 0
fi
if [[ -r /proc/cpuinfo ]]; then PF_VIRT_KIND=bare-metal; fi
return 0
}
pf_is_container() { [[ $PF_VIRT_KIND == container ]]; }
pf_report_env() {
pf_detect_env
case $PF_VIRT_KIND in
container) pf_info "Umgebung: Container${PF_VIRT_NAME:+ ($PF_VIRT_NAME)}." ;;
vm) pf_info "Umgebung: virtuelle Maschine${PF_VIRT_NAME:+ ($PF_VIRT_NAME)}." ;;
bare-metal) pf_info "Umgebung: Hardware (weder VM noch Container erkannt)." ;;
*) pf_info "Umgebung: nicht erkennbar." ;;
esac
}
pf_check_pypi() {
local ok=0 hint="Proxy setzen (export https_proxy=http://proxy:3128 bzw. pip --proxy) oder einen internen PyPI-Mirror verwenden; ohne Zugriff schlägt pip install fehl."
if command -v curl >/dev/null 2>&1; then
if curl -sS -o /dev/null --max-time "$PF_NET_TIMEOUT" --proto '=https' --tlsv1.2 -I "$PF_PYPI_URL" >/dev/null 2>&1; then ok=1; fi
elif command -v python3 >/dev/null 2>&1; then
if python3 -I -c 'import sys, urllib.request; urllib.request.urlopen(sys.argv[1], timeout=float(sys.argv[2])).close()' "$PF_PYPI_URL" "$PF_NET_TIMEOUT" >/dev/null 2>&1; then ok=1; fi
else
pf_warn "pypi.org nicht prüfbar (weder curl noch python3 vorhanden)." "$hint"
return 0
fi
if (( ok )); then
pf_ok "pypi.org per HTTPS erreichbar."
else
pf_warn "pypi.org per HTTPS nicht erreichbar (Timeout ${PF_NET_TIMEOUT} s)." "$hint"
fi
}
# Gibt nur Host:Port aus (ohne Schema, Zugangsdaten, Pfad und Query).
pf_hostport() {
local url=$1 scheme="" rest host port=""
if [[ $url == *://* ]]; then scheme=${url%%://*}; rest=${url#*://}; else rest=$url; fi
rest=${rest%%/*}; rest=${rest%%\?*}; rest=${rest%%#*}
rest=${rest##*@}
if [[ $rest == \[*\]* ]]; then
host=${rest%%]*}; host=${host#[}
port=${rest#*]}; port=${port#:}
else
host=${rest%%:*}
[[ $rest == *:* ]] && port=${rest##*:}
fi
if [[ -z $port ]]; then
case ${scheme,,} in http) port=80 ;; *) port=443 ;; esac
fi
[[ -n $host && $port =~ ^[0-9]+$ ]] || return 1
printf '%s %s' "$host" "$port"
}
pf_check_tcp() {
local label=$1 url=$2 hp host port shown
if ! hp=$(pf_hostport "$url"); then
pf_warn "$label: Wert nicht auswertbar (erwartet: URL wie https://host:port)." "Variable prüfen."
return 0
fi
read -r host port <<<"$hp"
shown="$host:$port"
[[ $host == *:* ]] && shown="[$host]:$port"
if timeout "$PF_NET_TIMEOUT" bash -c 'exec 3<>"/dev/tcp/$1/$2"' _ "$host" "$port" >/dev/null 2>&1; then
pf_ok "$label erreichbar: $shown."
else
pf_warn "$label nicht erreichbar: $shown." "DNS, Routing und Firewall von diesem Rechner zu $shown prüfen."
fi
}
pf_check_services() {
if [[ -n ${CHECKMK_URL:-} ]]; then
pf_check_tcp "Checkmk" "$CHECKMK_URL"
else
pf_info "Checkmk-Erreichbarkeit übersprungen (CHECKMK_URL nicht gesetzt)."
fi
if [[ -n ${OLLAMA_URL:-} ]]; then
pf_check_tcp "Ollama" "$OLLAMA_URL"
else
pf_info "Ollama-Erreichbarkeit übersprungen (OLLAMA_URL nicht gesetzt)."
fi
}
pf_report_git() {
local rev
if command -v git >/dev/null 2>&1 && git -c safe.directory="$PF_REPO_DIR" -C "$PF_REPO_DIR" rev-parse --git-dir >/dev/null 2>&1; then
rev=$(git -c safe.directory="$PF_REPO_DIR" -C "$PF_REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo unbekannt)
if [[ -n $(git -c safe.directory="$PF_REPO_DIR" -C "$PF_REPO_DIR" status --porcelain 2>/dev/null) ]]; then
pf_info "Git-Revision: $rev (mit nicht committeten Änderungen)."
else
pf_info "Git-Revision: $rev (sauber)."
fi
else
pf_info "Git-Revision: kein Git-Checkout oder git fehlt."
fi
}
# Aufruf: preflight_run <Zielverzeichnis>... ; /tmp wird immer mitgeprüft. Rückgabe 1 bei mindestens einem FEHLER.
preflight_run() {
local dir
PF_ERRORS=0
PF_WARNINGS=0
if [[ -n $PF_REPORT ]]; then : > "$PF_REPORT"; fi
pf_emit "== Preflight $(date '+%Y-%m-%d %H:%M:%S')"
pf_check_python
for dir in "$@" /tmp; do pf_check_disk_dir "$dir"; done
pf_check_os
pf_report_env
pf_check_pypi
pf_check_services
pf_report_git
pf_emit "== Preflight: $PF_ERRORS Fehler, $PF_WARNINGS Warnungen"
(( PF_ERRORS == 0 ))
}