#!/usr/bin/env bash # # install-lxc.sh — Erstellt einen LXC-Container auf einem Proxmox-VE-Host # und installiert den PVE Backup Log Viewer darin. # # Aufruf-Varianten: # # Lokal (Projekt-Ordner entpackt): # bash install-lxc.sh # # Direkt aus dem Netz (lädt app.py / service / requirements automatisch): # bash -c "$(curl -fsSL https://dc8wan.de/backup-dashboard/install-lxc.sh)" # # Modi: # - Interaktiv: whiptail-Menü mit Default- oder Advanced-Einstellungen # - Skriptgesteuert: alle Werte über Umgebungsvariablen vorgeben + # NONINTERACTIVE=1 setzen, um das Menü zu überspringen # # Umgebungsvariablen (alle optional, überschreiben Whiptail-Defaults): # CTID, HOSTNAME, DISK_SIZE, CORES, MEMORY, STORAGE, TEMPLATE_STORAGE, # BRIDGE, IP, GW, TASK_DIR, APP_PORT, READONLY_MOUNT, BASE_URL, # NONINTERACTIVE # set -euo pipefail # --- Konfiguration --------------------------------------------------------- APP_NAME="PVE Backup Log Viewer" BASE_URL_DEFAULT="https://dc8wan.de/backup-dashboard" CTID="${CTID:-}" HOSTNAME="${HOSTNAME:-backup-dashboard}" DISK_SIZE="${DISK_SIZE:-8}" CORES="${CORES:-1}" MEMORY="${MEMORY:-256}" STORAGE="${STORAGE:-local-lvm}" TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}" BRIDGE="${BRIDGE:-vmbr0}" IP="${IP:-dhcp}" GW="${GW:-}" TASK_DIR="${TASK_DIR:-/var/log/pve/tasks}" PBS_TASK_DIR="${PBS_TASK_DIR:-/var/log/proxmox-backup/tasks}" APP_PORT="${APP_PORT:-5000}" READONLY_MOUNT="${READONLY_MOUNT:-yes}" BASE_URL="${BASE_URL:-$BASE_URL_DEFAULT}" NONINTERACTIVE="${NONINTERACTIVE:-0}" # --- Farben + Output-Helfer ------------------------------------------------ YW=$'\033[33m'; GN=$'\033[32m'; RD=$'\033[31m'; BL=$'\033[34m' DG=$'\033[90m'; BD=$'\033[1m'; CL=$'\033[0m' header() { clear 2>/dev/null || true echo -e "${BL}" cat <<'BANNER' ____ ____ ____ _ _ _ | _ \| __ )/ ___| | | ___ __ _| | | |_ ___ _____ _ __ | |_) | _ \\___ \ | | / _ \ / _` | | | \ \ / / |/ _ \ \ /\ / / _ \ '__| | __/| |_) |___) | | |__| (_) | (_| | | | |\ V /| | __/\ V V / __/ | |_| |____/|____/ |_____\___/ \__, |_| |_| \_/ |_|\___| \_/\_/ \___|_| |___/ BANNER echo -e " ${BD}LXC-Installer für ${APP_NAME}${CL}" echo -e "${CL}" } msg_info() { echo -e " ${YW}-${CL} $1"; } msg_ok() { echo -e " ${GN}✓${CL} $1"; } msg_warn() { echo -e " ${YW}!${CL} $1"; } msg_error() { echo -e " ${RD}✗${CL} $1" >&2; } cleanup_on_error() { local rc=$? if [ -n "${TMP_FILES_DIR:-}" ] && [ -d "$TMP_FILES_DIR" ]; then rm -rf "$TMP_FILES_DIR" fi exit $rc } trap cleanup_on_error ERR # --- Sanity-Checks --------------------------------------------------------- header if [ "$EUID" -ne 0 ]; then msg_error "Bitte als root ausführen." exit 1 fi if ! command -v pct >/dev/null 2>&1; then msg_error "Dieses Script muss auf einem PVE-Host laufen ('pct' nicht gefunden)." exit 1 fi if ! command -v whiptail >/dev/null 2>&1; then msg_warn "whiptail nicht gefunden, Menü deaktiviert. Verwende Defaults / Env-Variablen." NONINTERACTIVE=1 fi if ! command -v curl >/dev/null 2>&1; then msg_error "'curl' nicht gefunden." exit 1 fi # --- Local vs. Remote ------------------------------------------------------ # Lokal: Script wurde aus einer Datei aufgerufen, neben der die Projekt- # Dateien liegen. Sonst: Files von BASE_URL nachladen. SCRIPT_DIR="" if [ -n "${BASH_SOURCE[0]:-}" ] && [ -f "${BASH_SOURCE[0]}" ]; then SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" fi if [ -n "$SCRIPT_DIR" ] \ && [ -f "$SCRIPT_DIR/app.py" ] \ && [ -f "$SCRIPT_DIR/requirements.txt" ] \ && [ -f "$SCRIPT_DIR/pve-log-viewer.service" ]; then SOURCE_MODE="local" FILES_DIR="$SCRIPT_DIR" else SOURCE_MODE="remote" FILES_DIR="" # später ausgefüllt fi # --- Container-ID + Validierung initialisieren ----------------------------- NEXT_FREE_ID="$(pvesh get /cluster/nextid 2>/dev/null || echo 100)" if [ -z "$CTID" ]; then CTID="$NEXT_FREE_ID" fi if [ ! -d "$TASK_DIR" ]; then msg_warn "PVE TASK_DIR existiert nicht: $TASK_DIR" fi # PBS-Quelle nur einbinden, wenn auf diesem Host vorhanden HAS_PBS=0 if [ -d "$PBS_TASK_DIR" ]; then HAS_PBS=1 fi # --- Whiptail-Wrapper ------------------------------------------------------ WT_TITLE="${APP_NAME} Installer" WT_BG="LXC bereitstellen und Service installieren" wt_input() { # wt_input "Beschreibung" "Vorgabewert" -> echo'd Wert local prompt="$1" default="$2" whiptail --backtitle "$WT_BG" --title "$WT_TITLE" \ --inputbox "$prompt" 10 70 "$default" 3>&1 1>&2 2>&3 } wt_menu() { # wt_menu "Beschreibung" "tag1" "label1" "tag2" "label2" ... local prompt="$1"; shift whiptail --backtitle "$WT_BG" --title "$WT_TITLE" \ --menu "$prompt" 16 70 6 "$@" 3>&1 1>&2 2>&3 } wt_yesno() { # wt_yesno "Beschreibung" -> Exit-Code 0 (yes) oder 1 (no) whiptail --backtitle "$WT_BG" --title "$WT_TITLE" \ --yesno "$1" 11 70 } # --- Menü ------------------------------------------------------------------ run_menu() { local mode mode=$(wt_menu "Wie möchtest du den Container konfigurieren?" \ "default" "Standard-Einstellungen (Schnell-Setup)" \ "advanced" "Erweiterte Einstellungen (alles anpassen)" \ "exit" "Abbrechen") || exit 0 case "$mode" in exit) exit 0 ;; default) : ;; # bleibe bei aktuellen Werten advanced) advanced_menu ;; esac summary_and_confirm } advanced_menu() { CTID=$(wt_input "Container-ID" "$CTID") HOSTNAME=$(wt_input "Hostname" "$HOSTNAME") DISK_SIZE=$(wt_input "rootfs-Größe (GB)" "$DISK_SIZE") CORES=$(wt_input "CPU-Kerne" "$CORES") MEMORY=$(wt_input "RAM (MB)" "$MEMORY") STORAGE=$(wt_input "Storage für rootfs" "$STORAGE") BRIDGE=$(wt_input "Netzwerk-Bridge" "$BRIDGE") local ip_mode ip_mode=$(wt_menu "IP-Konfiguration" \ "dhcp" "DHCP" \ "static" "Statisch") if [ "$ip_mode" = "dhcp" ]; then IP="dhcp"; GW="" else local ip_default="$IP"; [ "$ip_default" = "dhcp" ] && ip_default="192.168.1.100/24" IP=$(wt_input "Statische IP (CIDR, z.B. 192.168.11.240/24)" "$ip_default") GW=$(wt_input "Gateway" "$GW") fi TASK_DIR=$(wt_input "Host-Pfad der Task-Logs" "$TASK_DIR") APP_PORT=$(wt_input "App-Port (im Container)" "$APP_PORT") READONLY_MOUNT=$(wt_menu "Bind-Mount auf $TASK_DIR" \ "yes" "Nur Lesen (empfohlen)" \ "no" "Lesen und Schreiben") } summary_and_confirm() { local network if [ "$IP" = "dhcp" ]; then network="DHCP via $BRIDGE" else network="$IP via $BRIDGE (gw $GW)" fi local mount_desc="$TASK_DIR (read-only)" [ "$READONLY_MOUNT" = "no" ] && mount_desc="$TASK_DIR (read-write)" if [ "$HAS_PBS" = "1" ]; then local pbs_desc="$PBS_TASK_DIR (read-only)" [ "$READONLY_MOUNT" = "no" ] && pbs_desc="$PBS_TASK_DIR (read-write)" mount_desc="$mount_desc + $pbs_desc" fi local source_desc="lokal aus $FILES_DIR" [ "$SOURCE_MODE" = "remote" ] && source_desc="$BASE_URL" local summary summary=$(cat </dev/null 2>&1; then msg_error "Container $CTID existiert bereits." exit 1 fi if [ "$IP" != "dhcp" ] && [ -z "$GW" ]; then msg_error "Statische IP angegeben ($IP), aber GW fehlt." exit 1 fi if [ ! -d "$TASK_DIR" ]; then msg_error "TASK_DIR existiert nicht: $TASK_DIR" exit 1 fi # --- Files besorgen -------------------------------------------------------- if [ "$SOURCE_MODE" = "remote" ]; then msg_info "Lade Projekt-Dateien von $BASE_URL ..." TMP_FILES_DIR=$(mktemp -d -t pvelv-XXXXXX) FILES_DIR="$TMP_FILES_DIR" for f in app.py requirements.txt pve-log-viewer.service; do if ! curl -fsSL "$BASE_URL/$f" -o "$FILES_DIR/$f"; then msg_error "Download fehlgeschlagen: $BASE_URL/$f" exit 1 fi done msg_ok "Dateien heruntergeladen" else msg_ok "Dateien lokal in $FILES_DIR" fi # --- Template prüfen / laden ----------------------------------------------- EXISTING=$(pveam list "$TEMPLATE_STORAGE" 2>/dev/null \ | awk '/debian-12-standard.*amd64/ {print $1}' \ | sort -V | tail -1 || true) if [ -n "$EXISTING" ]; then TEMPLATE_PATH="$EXISTING" msg_ok "Template vorhanden: $(basename "$TEMPLATE_PATH")" else msg_info "Aktualisiere Template-Liste..." pveam update >/dev/null TPL=$(pveam available -section system 2>/dev/null \ | awk '/debian-12-standard.*amd64/ {print $2}' \ | sort -V | tail -1) if [ -z "$TPL" ]; then msg_error "Kein debian-12-standard-amd64-Template gefunden." exit 1 fi msg_info "Lade $TPL ..." pveam download "$TEMPLATE_STORAGE" "$TPL" >/dev/null TEMPLATE_PATH="${TEMPLATE_STORAGE}:vztmpl/${TPL}" msg_ok "Template heruntergeladen" fi # --- Container-Parameter --------------------------------------------------- if [ "$IP" = "dhcp" ]; then NET="name=eth0,bridge=${BRIDGE},firewall=1,ip=dhcp,type=veth" else NET="name=eth0,bridge=${BRIDGE},firewall=1,gw=${GW},ip=${IP},type=veth" fi MP0="${TASK_DIR},mp=${TASK_DIR},acl=1" [ "$READONLY_MOUNT" = "yes" ] && MP0="${MP0},ro=1" MP_ARGS=( --mp0 "$MP0" ) if [ "$HAS_PBS" = "1" ]; then MP1="${PBS_TASK_DIR},mp=${PBS_TASK_DIR},acl=1" [ "$READONLY_MOUNT" = "yes" ] && MP1="${MP1},ro=1" MP_ARGS+=( --mp1 "$MP1" ) msg_info "PBS-Verzeichnis erkannt – wird zusätzlich gemountet ($PBS_TASK_DIR)" fi # --- Container erstellen --------------------------------------------------- msg_info "Erstelle Container $CTID ($HOSTNAME)..." pct create "$CTID" "$TEMPLATE_PATH" \ --hostname "$HOSTNAME" \ --arch amd64 \ --ostype debian \ --cores "$CORES" \ --memory "$MEMORY" \ --swap 0 \ --rootfs "${STORAGE}:${DISK_SIZE}" \ --features nesting=1 \ --net0 "$NET" \ "${MP_ARGS[@]}" \ --onboot 1 \ --unprivileged 0 \ --start 1 >/dev/null msg_ok "Container erstellt und gestartet" # --- Auf Netzwerk warten --------------------------------------------------- msg_info "Warte auf Netzwerk..." for i in {1..30}; do if pct exec "$CTID" -- getent hosts deb.debian.org >/dev/null 2>&1; then msg_ok "Netzwerk erreichbar" break fi sleep 1 if [ "$i" -eq 30 ]; then msg_error "Netzwerk nach 30s nicht erreichbar." exit 1 fi done # --- Files in den CT schieben ---------------------------------------------- msg_info "Kopiere Projekt-Dateien in den Container..." pct exec "$CTID" -- mkdir -p /opt/pve-log-viewer pct push "$CTID" "$FILES_DIR/app.py" /opt/pve-log-viewer/app.py pct push "$CTID" "$FILES_DIR/requirements.txt" /opt/pve-log-viewer/requirements.txt pct push "$CTID" "$FILES_DIR/pve-log-viewer.service" /etc/systemd/system/pve-log-viewer.service msg_ok "Dateien kopiert" # --- Pakete + venv + Service ----------------------------------------------- msg_info "Installiere python3-venv..." pct exec "$CTID" -- bash -c " set -e export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq python3-venv >/dev/null " >/dev/null msg_ok "Pakete installiert" msg_info "Erstelle venv und installiere Flask..." pct exec "$CTID" -- bash -c " set -e cd /opt/pve-log-viewer python3 -m venv venv ./venv/bin/pip install --disable-pip-version-check -q -r requirements.txt chown -R www-data:www-data /opt/pve-log-viewer " >/dev/null msg_ok "venv eingerichtet" msg_info "Aktiviere systemd-Service..." pct exec "$CTID" -- bash -c " systemctl daemon-reload systemctl enable --now pve-log-viewer " >/dev/null sleep 2 if pct exec "$CTID" -- systemctl is-active pve-log-viewer >/dev/null 2>&1; then msg_ok "Service läuft" else msg_error "Service ist nicht aktiv. Im Container prüfen:" msg_error " pct enter $CTID" msg_error " journalctl -u pve-log-viewer -n 30" fi # --- Aufräumen + Abschluss ------------------------------------------------- if [ -n "${TMP_FILES_DIR:-}" ] && [ -d "$TMP_FILES_DIR" ]; then rm -rf "$TMP_FILES_DIR" fi trap - ERR CT_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}') echo "" msg_ok "Fertig." echo "" echo -e " ${GN}URL:${CL} ${BD}http://${CT_IP}:${APP_PORT}${CL}" echo -e " ${GN}Container:${CL} $CTID ($HOSTNAME)" echo -e " ${GN}rootfs:${CL} ${STORAGE} (${DISK_SIZE} GB)" echo -e " ${GN}Mount PVE:${CL} ${TASK_DIR} → ${TASK_DIR}$([ "$READONLY_MOUNT" = "yes" ] && echo " (ro)")" if [ "$HAS_PBS" = "1" ]; then echo -e " ${GN}Mount PBS:${CL} ${PBS_TASK_DIR} → ${PBS_TASK_DIR}$([ "$READONLY_MOUNT" = "yes" ] && echo " (ro)")" fi echo -e " ${GN}Service:${CL} ${DG}systemctl status pve-log-viewer${CL} (im CT)" echo -e " ${GN}Shell:${CL} ${DG}pct enter $CTID${CL}" echo ""