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

279 lines
9.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Erstinstallation von checkbot in einer frischen Debian-12/13-VM oder einem LXC (auch Ubuntu ab 22.04). Als root aus dem Git-Checkout starten.
set -euo pipefail
SELF=$(readlink -f "${BASH_SOURCE[0]}")
# shellcheck source=lib/common.sh
source "$(dirname "$SELF")/lib/common.sh"
# shellcheck source=lib/caddy.sh
source "$COMMON_DIR/caddy.sh"
# shellcheck source=lib/timezone.sh
source "$COMMON_DIR/timezone.sh"
# shellcheck source=lib/preflight.sh
source "$COMMON_DIR/preflight.sh"
PACKAGES=(python3 python3-venv python3-pip ca-certificates curl rsync tzdata)
CLI_LINK=/usr/local/sbin/checkbot-cli
RELAXED_DROPIN_SRC=$DEPLOY_DIR/systemd/checkbot-lxc-relaxed.conf
RELAXED_DROPIN_DST=$SYSTEMD_DIR/checkbot.service.d/lxc-relaxed.conf
BACKUP_RELAXED_DROPIN_DST=$SYSTEMD_DIR/checkbot-backup.service.d/lxc-relaxed.conf
SECRET_FILES=(master.key session.secret)
HOSTNAME_RE='^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$|^[0-9A-Fa-f:]+$'
IP_LIST_RE='^[0-9A-Fa-f:./ ,]*$'
PATH_RE='^[A-Za-z0-9_./-]+$'
FQDN=${CHECKBOT_FQDN:-}
MONITOR_IP=${CHECKBOT_MONITOR_IP:-}
TLS_MODE=${CHECKBOT_TLS_MODE:-internal}
TLS_CERT=${CHECKBOT_TLS_CERT:-}
TLS_KEY=${CHECKBOT_TLS_KEY:-}
TIMEZONE=${CHECKBOT_TZ:-Europe/Berlin}
ASSUME_NO_PROMPT=0
RELAXED=0
SKIP_PREFLIGHT=0
PREFLIGHT_ONLY=0
usage() {
cat <<USAGE
Aufruf: sudo deploy/setup-lxc.sh [Optionen]
Installiert checkbot (Code, venv, Schlüssel, systemd, Caddy) in einer Debian-12/13-VM oder einem LXC.
Mehrfaches Ausführen ist sicher; Vorhandenes (Schlüssel, checkbot.env) wird nicht überschrieben.
Optionen (Umgebungsvariable in Klammern):
--fqdn NAME Hostname der UI, Standard: hostname -f (CHECKBOT_FQDN)
--monitor-ip IPS Monitoring-Server für /health, mehrere mit Komma (CHECKBOT_MONITOR_IP)
--tls-mode MODUS internal (Caddy-interne CA, Standard) oder files (CHECKBOT_TLS_MODE)
--tls-cert DATEI Zertifikat (PEM) für --tls-mode files (CHECKBOT_TLS_CERT)
--tls-key DATEI privater Schlüssel (PEM) für --tls-mode files (CHECKBOT_TLS_KEY)
--tz ZONE Systemzeitzone, Standard Europe/Berlin (CHECKBOT_TZ)
--lxc-relaxed gelockertes systemd-Drop-In für unprivilegierte LXCs installieren
(nur bei Exit-Code 218/226/228; in einer VM nicht nötig)
--skip-preflight Preflight-Prüfungen überspringen
--preflight-only nur die Preflight-Prüfungen ausführen und beenden
--no-prompt nie nachfragen (z.B. wegen Admin-Anlage)
-h, --help diese Hilfe
Zusätzlich: CHECKBOT_CADDY_KEY_FPR pinnt den Fingerabdruck des Caddy-Repo-Schlüssels (nur falls Caddy
nicht im Debian-Repository liegt).
USAGE
}
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
--fqdn) FQDN=${2:?--fqdn braucht einen Wert}; shift 2 ;;
--monitor-ip) MONITOR_IP=${2:?--monitor-ip braucht einen Wert}; shift 2 ;;
--tls-mode) TLS_MODE=${2:?--tls-mode braucht einen Wert}; shift 2 ;;
--tls-cert) TLS_CERT=${2:?--tls-cert braucht einen Wert}; shift 2 ;;
--tls-key) TLS_KEY=${2:?--tls-key braucht einen Wert}; shift 2 ;;
--tz) TIMEZONE=${2:?--tz braucht einen Wert}; shift 2 ;;
--lxc-relaxed) RELAXED=1; shift ;;
--skip-preflight) SKIP_PREFLIGHT=1; shift ;;
--preflight-only) PREFLIGHT_ONLY=1; shift ;;
--no-prompt) ASSUME_NO_PROMPT=1; shift ;;
-h|--help) usage; exit 0 ;;
*) usage >&2; die "Unbekannte Option: $1" ;;
esac
done
}
validate_args() {
(( ! SKIP_PREFLIGHT || ! PREFLIGHT_ONLY )) || die "--skip-preflight und --preflight-only schließen sich aus."
if [[ -z $FQDN ]]; then
FQDN=$(hostname -f 2>/dev/null || hostname)
fi
[[ $FQDN =~ $HOSTNAME_RE ]] || die "Ungültiger Hostname für die UI: '$FQDN'"
validate_timezone_name "$TIMEZONE"
MONITOR_IP=${MONITOR_IP//,/ }
[[ $MONITOR_IP =~ $IP_LIST_RE ]] || die "Ungültige Monitoring-IP: '$MONITOR_IP'"
case $TLS_MODE in
internal) ;;
files)
[[ -f $TLS_CERT && -f $TLS_KEY ]] || die "TLS-Modus files braucht --tls-cert und --tls-key (lesbare Dateien)."
[[ $TLS_CERT =~ $PATH_RE && $TLS_KEY =~ $PATH_RE ]] || die "Zertifikatspfade dürfen nur Buchstaben, Ziffern und ._/- enthalten."
;;
*) die "Ungültiger TLS-Modus '$TLS_MODE' (internal oder files)." ;;
esac
}
run_preflight() {
(( ! SKIP_PREFLIGHT )) || { pf_detect_env; return 0; }
PF_PACKAGES_PENDING=1
preflight_run "$APP_DIR" "$DATA_DIR" || die "Preflight fehlgeschlagen (FEHLER-Zeilen oben). --skip-preflight übergeht die Prüfung."
}
install_packages() {
log "Installiere Pakete."
apt-get update -qq
apt_install "${PACKAGES[@]}"
}
ensure_user_and_dirs() {
if ! getent passwd "$APP_USER" >/dev/null; then
log "Lege Systembenutzer $APP_USER an."
useradd --system --user-group --home-dir "$DATA_DIR" --no-create-home --shell /usr/sbin/nologin "$APP_USER"
fi
install -d -m 0750 -o root -g "$APP_GROUP" "$CONF_DIR"
install -d -m 0750 -o "$APP_USER" -g "$APP_GROUP" "$DATA_DIR"
install -d -m 0700 -o "$APP_USER" -g "$APP_GROUP" "$BACKUP_DIR"
}
# Das Konfigurationsverzeichnis gehört root; checkbot kann dort keine Dateien anlegen, daher als root erzeugen und übergeben.
ensure_keys() {
local name
(
umask 077
cd "$APP_DIR"
CHECKBOT_CONFIG_DIR=$CONF_DIR PYTHONDONTWRITEBYTECODE=1 "$VENV_DIR/bin/python" -m app.cli gen-keys
)
for name in "${SECRET_FILES[@]}"; do
[[ -f $CONF_DIR/$name ]] || die "$CONF_DIR/$name fehlt nach gen-keys."
chown "$APP_USER:$APP_GROUP" "$CONF_DIR/$name"
chmod 0600 "$CONF_DIR/$name"
done
}
ensure_env_file() {
if [[ -f $ENV_FILE ]]; then
log "$ENV_FILE existiert, bleibt unverändert."
else
install -m 0640 -o root -g "$APP_GROUP" "$DEPLOY_DIR/checkbot.env.example" "$ENV_FILE"
log "$ENV_FILE angelegt."
fi
chown root:"$APP_GROUP" "$ENV_FILE"
chmod 0640 "$ENV_FILE"
}
install_dropins() {
if (( RELAXED )); then
if ! pf_is_container; then
warn "--lxc-relaxed ohne erkannten Container (Umgebung: $PF_VIRT_KIND); das Drop-In schwächt die Sandbox und wird nur auf ausdrücklichen Wunsch installiert."
fi
install -D -m 0644 "$RELAXED_DROPIN_SRC" "$RELAXED_DROPIN_DST"
install -D -m 0644 "$RELAXED_DROPIN_SRC" "$BACKUP_RELAXED_DROPIN_DST"
systemctl daemon-reload
log "Gelockertes systemd-Drop-In installiert."
elif [[ -e $RELAXED_DROPIN_DST ]]; then
warn "$RELAXED_DROPIN_DST existiert noch (frühere Installation mit --lxc-relaxed) und lockert die Sandbox weiter."
fi
}
install_cli_link() {
ln -sfn "$APP_DIR/deploy/checkbot-cli" "$CLI_LINK"
}
configure_caddy() {
local port host upstream rendered
ensure_caddy
port=$(env_value CHECKBOT_BIND_PORT "$DEFAULT_BIND_PORT")
host=$(env_value CHECKBOT_BIND_HOST "$DEFAULT_BIND_HOST")
case $host in 0.0.0.0|::|"") host=127.0.0.1 ;; esac
[[ $host == *:* ]] && host="[$host]"
upstream="$host:$port"
if [[ $TLS_MODE == files ]]; then
install_tls_files "$TLS_CERT" "$TLS_KEY"
fi
rendered=$(render_caddyfile "$FQDN" "$TLS_MODE" "$MONITOR_IP" "$upstream")
apply_caddyfile "$rendered"
}
start_service() {
systemctl enable "$SERVICE_NAME" >/dev/null
systemctl enable --now checkbot-backup.timer >/dev/null
systemctl restart "$SERVICE_NAME"
if ! wait_health; then
warn "Dienst antwortet nicht innerhalb von ${HEALTH_WAIT_SECONDS} s."
warn "Logs: journalctl -u $SERVICE_NAME -n 100 --no-pager."
if pf_is_container; then
warn "Bei Exit-Code 218/226/228 im Container: --lxc-relaxed (siehe deploy/README.md)."
fi
return 1
fi
}
has_admin() {
local db
db=$(db_file)
[[ -f $db ]] || return 1
runuser -u "$APP_USER" -- python3 -I -c '
import sqlite3, sys
try:
n = sqlite3.connect(sys.argv[1]).execute("SELECT COUNT(*) FROM users WHERE role = ? AND disabled = 0", ("admin",)).fetchone()[0]
except sqlite3.Error:
n = 0
sys.exit(0 if n else 1)
' "$db"
}
offer_admin() {
local answer=""
if has_admin; then
log "Ein Admin existiert bereits."
return 0
fi
if (( ! ASSUME_NO_PROMPT )) && [[ -t 0 && -t 1 ]]; then
read -r -p "Ersten Admin jetzt anlegen? [J/n] " answer || answer=n
if [[ ! $answer =~ ^[nN] ]]; then
"$APP_DIR/deploy/checkbot-cli" create-admin && return 0
warn "Admin-Anlage fehlgeschlagen. Später: sudo checkbot-cli create-admin"
fi
fi
ADMIN_PENDING=1
}
summary() {
cat <<SUMMARY
checkbot ist installiert.
UI: https://$FQDN/
Dienst: systemctl status $SERVICE_NAME
Logs: journalctl -u $SERVICE_NAME -f
Backups: täglich nach $BACKUP_DIR (Timer checkbot-backup.timer)
SUMMARY
if (( ${ADMIN_PENDING:-0} )); then
printf 'Noch offen: Admin anlegen mit\n sudo checkbot-cli create-admin\n\n'
fi
cat <<SUMMARY
Nächste Schritte:
1. In der UI anmelden und den Setup-Assistenten durchlaufen.
2. Checkmk: Automation-User mit Lese-Rolle anlegen und in der UI eintragen.
3. Telegram: Bot bei @BotFather anlegen, Token und Chat-ID in der UI eintragen (optional Matrix).
4. Ollama-Host (und ggf. Token/Proxy) in der UI eintragen.
5. master.key und session.secret aus $CONF_DIR getrennt von der DB sichern.
SUMMARY
if [[ $TLS_MODE == internal ]]; then
printf ' 6. Root-Zertifikat der Caddy-CA an die Clients verteilen: /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt\n'
fi
}
main() {
case ${1:-} in -h|--help) usage; return 0 ;; esac
parse_args "$@"
require_root
validate_args
run_preflight
if (( PREFLIGHT_ONLY )); then return 0; fi
umask 022
install_packages
ensure_timezone "$TIMEZONE"
check_python_version
ensure_user_and_dirs
sync_code
ensure_venv
ensure_keys
ensure_env_file
install_units
install_dropins
install_cli_link
configure_caddy
start_service
ADMIN_PENDING=0
offer_admin
summary
}
main "$@"