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>
257 lines
9.1 KiB
Bash
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 ))
|
|
}
|