commit 13ffdff7dada3f4ce619af0ca665f111621e8cf4 Author: RolandS <1+xosna@noreply.localhost> Date: Mon May 25 06:19:45 2026 +0200 multi-os-install.sh hinzugefügt diff --git a/multi-os-install.sh b/multi-os-install.sh new file mode 100644 index 0000000..2144033 --- /dev/null +++ b/multi-os-install.sh @@ -0,0 +1,767 @@ +#!/usr/bin/env bash +# ============================================================================== +# Proxmox LXC – Multi-OS Installer +# Unterstützt: Debian / Ubuntu (immer aktuelle + vor/nachherige Version) +# Aufruf auf dem PVE-Host: +# bash <(curl -fsSL https://your-host/lxc-install.sh) +# ============================================================================== + +set -euo pipefail + +# ── Farben ───────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +CYAN='\033[0;36m'; BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m' + +info() { echo -e "${CYAN}[INFO]${NC} $*"; } +success() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } + +# ── Voraussetzungen ──────────────────────────────────────────────────────────── +[[ $EUID -ne 0 ]] && error "Muss als root auf dem PVE-Host ausgeführt werden." +command -v pct &>/dev/null || error "'pct' nicht gefunden – kein PVE-Host?" +command -v pvesh &>/dev/null || error "'pvesh' nicht gefunden – kein PVE-Host?" + +# ══════════════════════════════════════════════════════════════════════════════ +clear +echo -e "${BOLD}" +echo -e " ╔══════════════════════════════════════════════════════════╗" +echo -e " ║ Proxmox LXC – Multi-OS Installer ║" +echo -e " ║ Debian • Ubuntu | + Docker • Portainer ║" +echo -e " ╚══════════════════════════════════════════════════════════╝" +echo -e "${NC}" + +# ── Hilfsfunktionen ──────────────────────────────────────────────────────────── +ask() { + local prompt="$1" default="$2" var_name="$3" + echo -ne "${BOLD}${prompt}${NC} [${YELLOW}${default}${NC}]: " + read -r input + printf -v "$var_name" '%s' "${input:-$default}" +} + +ask_yn() { + local prompt="$1" default="${2:-j}" + echo -ne "${BOLD}${prompt}${NC} [${YELLOW}${default}${NC}]: " + read -r yn + [[ "${yn:-$default}" =~ ^[Jj1Yy] ]] +} + +# ══════════════════════════════════════════════════════════════════════════════ +# SCHRITT 1 – DISTRIBUTION WÄHLEN +# ══════════════════════════════════════════════════════════════════════════════ +echo -e "${CYAN}╔══ Schritt 1: Distribution ═══════════════════════════════════╗${NC}" +echo -e "" +echo -e " ${BOLD}[1]${NC} 🌀 ${BOLD}Debian${NC} – schlank, stabil, bewährt" +echo -e " ${BOLD}[2]${NC} 🟠 ${BOLD}Ubuntu${NC} – breite Paketbasis, LTS-Support" +echo -e "" +while true; do + echo -ne "${BOLD} Auswahl${NC} [${YELLOW}1${NC}]: " + read -r dist_sel + case "${dist_sel:-1}" in + 1) DISTRO="debian"; DISTRO_LABEL="Debian"; DISTRO_MATCH="debian"; break ;; + 2) DISTRO="ubuntu"; DISTRO_LABEL="Ubuntu"; DISTRO_MATCH="ubuntu"; break ;; + *) warn "Bitte 1 oder 2 eingeben." ;; + esac +done +echo "" + +# ══════════════════════════════════════════════════════════════════════════════ +# SCHRITT 2 – VERSION WÄHLEN (live aus PVE-Datenbank, immer aktuell) +# ══════════════════════════════════════════════════════════════════════════════ +echo -e "${CYAN}╔══ Schritt 2: Version ════════════════════════════════════════╗${NC}" +echo "" +info "Lese verfügbare ${DISTRO_LABEL}-Templates aus der PVE-Datenbank …" + +# Template-Liste aktualisieren (leise, falls nötig) +pveam update &>/dev/null || true + +# Alle passenden Templates holen, nach Versionsnummer sortieren +mapfile -t ALL_TEMPLATES < <( + pveam available --section system 2>/dev/null \ + | awk '{print $2}' \ + | grep -i "^${DISTRO_MATCH}-" \ + | sort -V +) + +[[ ${#ALL_TEMPLATES[@]} -eq 0 ]] && error "Keine ${DISTRO_LABEL}-Templates gefunden. Bitte 'pveam update' ausführen." + +# ── Versionsnummern extrahieren und deduplizieren ────────────────────────────── +# Debian-Muster: debian-12-standard_12.7-1_amd64.tar.zst → major=12 +# Ubuntu-Muster: ubuntu-24.04-standard_24.04-1_amd64.tar.zst → major=24.04 +declare -A VER_TO_TEMPLATE # major_version → neuestes Template +declare -a VER_ORDER=() + +for t in "${ALL_TEMPLATES[@]}"; do + if [[ "$DISTRO" == "debian" ]]; then + ver=$(echo "$t" | grep -oP 'debian-\K[0-9]+' | head -1) + else + ver=$(echo "$t" | grep -oP 'ubuntu-\K[0-9]+\.[0-9]+' | head -1) + fi + [[ -z "$ver" ]] && continue + # Letztes (neustes) Template pro Version merken (sort -V sorgt dafür) + VER_TO_TEMPLATE["$ver"]="$t" + # Version in geordneter Liste nur einmal speichern + if [[ ! " ${VER_ORDER[*]} " =~ " ${ver} " ]]; then + VER_ORDER+=("$ver") + fi +done + +[[ ${#VER_ORDER[@]} -eq 0 ]] && error "Konnte keine Versionsnummern aus den Templates extrahieren." + +# ── Die 3 relevantesten Versionen bestimmen: vorherige / aktuelle / nächste ──── +# "Aktuell" = höchste stabile Version (letzte in der sortierten Liste) +# "Vorherige" = eine davor +# "Nächste" = falls vorhanden (Alpha/Beta/RC Templates in PVE) +TOTAL=${#VER_ORDER[@]} +CURRENT_IDX=$(( TOTAL - 1 )) + +# Prüfen ob es eine "nächste" Version gibt (höher als die letzte stabile) +# PVE-Datenbank enthält manchmal schon Beta-Templates → nehmen wir mit +NEXT_IDX=$(( CURRENT_IDX + 1 )) # existiert nicht → wird unten abgefangen +PREV_IDX=$(( CURRENT_IDX - 1 )) + +# Verfügbare Slots bauen +declare -a SHOW_VERS=() +declare -a SHOW_LABELS=() + +# Vorherige Version (falls vorhanden) +if (( PREV_IDX >= 0 )); then + SHOW_VERS+=("${VER_ORDER[$PREV_IDX]}") + SHOW_LABELS+=("Vorherige Version (${VER_ORDER[$PREV_IDX]})") +fi + +# Aktuelle Version (Standard) +SHOW_VERS+=("${VER_ORDER[$CURRENT_IDX]}") +SHOW_LABELS+=("Aktuelle Version (${VER_ORDER[$CURRENT_IDX]}) ← empfohlen") +DEFAULT_VER_IDX=$(( ${#SHOW_VERS[@]} )) # 1-basierter Default für die Ausgabe + +# Nächste Version (nur wenn tatsächlich ein neueres Template existiert) +# → In der Praxis liefert PVE hier manchmal schon den nächsten LTS +if (( NEXT_IDX < TOTAL )); then + SHOW_VERS+=("${VER_ORDER[$NEXT_IDX]}") + SHOW_LABELS+=("Nächste Version (${VER_ORDER[$NEXT_IDX]}) [experimentell]") +fi + +echo -e " Gefundene ${DISTRO_LABEL}-Versionen:\n" +for i in "${!SHOW_VERS[@]}"; do + num=$(( i + 1 )) + if (( num == DEFAULT_VER_IDX )); then + echo -e " ${BOLD}[${num}]${NC} ${GREEN}${SHOW_LABELS[$i]}${NC}" + else + echo -e " ${BOLD}[${num}]${NC} ${SHOW_LABELS[$i]}" + fi +done +echo "" + +while true; do + echo -ne "${BOLD} Auswahl${NC} [${YELLOW}${DEFAULT_VER_IDX}${NC}]: " + read -r ver_sel + ver_sel="${ver_sel:-$DEFAULT_VER_IDX}" + if [[ "$ver_sel" =~ ^[0-9]+$ ]] && (( ver_sel >= 1 && ver_sel <= ${#SHOW_VERS[@]} )); then + CHOSEN_VER="${SHOW_VERS[$((ver_sel-1))]}" + CHOSEN_TEMPLATE="${VER_TO_TEMPLATE[$CHOSEN_VER]}" + success "Gewählt: ${BOLD}${DISTRO_LABEL} ${CHOSEN_VER}${NC} → ${CHOSEN_TEMPLATE}" + break + else + warn "Bitte eine Zahl zwischen 1 und ${#SHOW_VERS[@]} eingeben." + fi +done +echo "" + +# ── Distro-spezifische Defaults ──────────────────────────────────────────────── +if [[ "$DISTRO" == "debian" ]]; then + DEFAULT_RAM=512; DEFAULT_DISK=4; DEFAULT_HOSTNAME="debian" + DISTRO_BADGE="https://img.shields.io/badge/Debian%20${CHOSEN_VER}-A81D33?style=for-the-badge&logo=debian&logoColor=white" + DISTRO_LINK="https://www.debian.org" + DOCS_LABEL="Debian Docs"; DOCS_URL="https://www.debian.org/releases/" + PKG_LABEL="Debian Packages"; PKG_URL="https://packages.debian.org" + SSH_CFG_MODE="sshd_config" # Debian: direkt in sshd_config +else + DEFAULT_RAM=1024; DEFAULT_DISK=8; DEFAULT_HOSTNAME="ubuntu-server" + VER_NICE="${CHOSEN_VER} LTS" + DISTRO_BADGE="https://img.shields.io/badge/Ubuntu%20${CHOSEN_VER// /%20}%20LTS-E95420?style=for-the-badge&logo=ubuntu&logoColor=white" + DISTRO_LINK="https://ubuntu.com/server" + DOCS_LABEL="Ubuntu Server Docs"; DOCS_URL="https://ubuntu.com/server/docs" + PKG_LABEL="Ubuntu Packages"; PKG_URL="https://packages.ubuntu.com" + SSH_CFG_MODE="sshd_config_d" # Ubuntu: Drop-in /etc/ssh/sshd_config.d/ +fi + +# ══════════════════════════════════════════════════════════════════════════════ +# SCHRITT 3 – CONTAINER-KONFIGURATION +# ══════════════════════════════════════════════════════════════════════════════ +echo -e "${CYAN}╔══ Schritt 3: Container-Konfiguration ════════════════════════╗${NC}" +echo "" + +DEFAULT_CTID=$(pvesh get /cluster/nextid) + +echo -e "${CYAN}── Grundkonfiguration ───────────────────────────────────────${NC}" +ask "Container-ID" "$DEFAULT_CTID" CTID +ask "Hostname" "$DEFAULT_HOSTNAME" HOSTNAME +ask "Root-Passwort" "ChangeMe123!" PASSWORD +echo "" + +# ── Storage-Auswahl ──────────────────────────────────────────────────────────── +echo -e "${CYAN}── Storage-Auswahl ──────────────────────────────────────────${NC}" +echo -e " Verfügbare Storages:\n" +mapfile -t STORAGE_LINES < <( + pvesm status --content rootdir 2>/dev/null \ + | awk 'NR>1 && $3=="active" {print $1, $2, $5, $4}' | sort +) +[[ ${#STORAGE_LINES[@]} -eq 0 ]] && mapfile -t STORAGE_LINES < <( + pvesm status 2>/dev/null | awk 'NR>1 && $3=="active" {print $1, $2, $5, $4}' | sort +) +if [[ ${#STORAGE_LINES[@]} -eq 0 ]]; then + warn "Keine Storages gefunden – manuelle Eingabe." + ask "Storage-Name" "local-lvm" STORAGE +else + local_human_size() { + local kb=$1 + (( kb >= 1073741824 )) && { printf "%.1f TiB" "$(echo "scale=1;$kb/1073741824"|bc)"; return; } + (( kb >= 1048576 )) && { printf "%.1f GiB" "$(echo "scale=1;$kb/1048576"|bc)"; return; } + (( kb >= 1024 )) && { printf "%.1f MiB" "$(echo "scale=1;$kb/1024"|bc)"; return; } + printf "%d KiB" "$kb" + } + declare -a ST_NAMES=() + printf " ${BOLD}%-4s %-20s %-14s %-12s %-12s${NC}\n" "Nr." "Name" "Typ" "Frei" "Gesamt" + echo -e " ──────────────────────────────────────────────────────────" + i=1 + for line in "${STORAGE_LINES[@]}"; do + read -r sname stype sused stotal <<< "$line" + sfree=0 + [[ "$stotal" =~ ^[0-9]+$ && "$sused" =~ ^[0-9]+$ ]] && sfree=$(( stotal - sused )) + if [[ "$stotal" =~ ^[0-9]+$ ]] && (( stotal > 0 )); then + fs=$(local_human_size "$sfree"); ts=$(local_human_size "$stotal") + pct=$(( sfree * 100 / stotal )) + col="$GREEN"; (( pct < 15 )) && col="$RED"; (( pct>=15 && pct<30 )) && col="$YELLOW" + else + fs="n/a"; ts="n/a"; col="$NC" + fi + printf " ${BOLD}%-4s${NC} ${col}%-20s${NC} %-14s %-12s %-12s\n" "[$i]" "$sname" "$stype" "$fs" "$ts" + ST_NAMES+=("$sname"); (( i++ )) + done + echo "" + while true; do + echo -ne "${BOLD} Auswahl${NC} [${YELLOW}1${NC}]: " + read -r sel; sel="${sel:-1}" + if [[ "$sel" =~ ^[0-9]+$ ]] && (( sel>=1 && sel<=${#ST_NAMES[@]} )); then + STORAGE="${ST_NAMES[$((sel-1))]}"; success "Storage: ${BOLD}${STORAGE}${NC}"; break + else warn "Ungültige Eingabe."; fi + done +fi +echo "" + +echo -e "${CYAN}── Ressourcen ───────────────────────────────────────────────${NC}" +ask "Disk-Größe (GB)" "$DEFAULT_DISK" DISK +ask "RAM (MB)" "$DEFAULT_RAM" RAM +ask "Swap (MB)" "512" SWAP +ask "CPU-Kerne" "2" CORES +ask "Netzwerk-Bridge" "vmbr0" BRIDGE +echo "" + +echo -e "${CYAN}── Netzwerk ─────────────────────────────────────────────────${NC}" +echo -e " DHCP: '${YELLOW}dhcp${NC}' | Statisch z.B.: '${YELLOW}192.168.1.100/24${NC}'" +ask "IP-Adresse" "dhcp" IP_ADDR +GATEWAY="" +[[ "$IP_ADDR" != "dhcp" ]] && ask "Gateway" "" GATEWAY +echo "" + +echo -e "${CYAN}── Container-Typ ────────────────────────────────────────────${NC}" +ask_yn "Unprivilegiert? (empfohlen)" "j" && UNPRIVILEGED=1 || UNPRIVILEGED=0 +echo "" + +echo -e "${CYAN}── Zeitzone ─────────────────────────────────────────────────${NC}" +ask "Zeitzone" "Europe/Berlin" TIMEZONE +echo "" + +# ══════════════════════════════════════════════════════════════════════════════ +# SCHRITT 4 – SOFTWARE +# ══════════════════════════════════════════════════════════════════════════════ +echo -e "${CYAN}╔══ Schritt 4: Software ════════════════════════════════════════╗${NC}" +echo "" + +# ── Docker ──────────────────────────────────────────────────────────────────── +echo -e "${CYAN}── Docker ───────────────────────────────────────────────────${NC}" +if ask_yn "🐳 Docker Engine installieren?" "j"; then + INSTALL_DOCKER=true + + echo "" + echo -e "${CYAN}── Portainer ────────────────────────────────────────────────${NC}" + if ask_yn "🖥 Portainer CE (Docker Web-UI) installieren?" "j"; then + INSTALL_PORTAINER=true + ask "Portainer HTTPS-Port" "9443" PORTAINER_PORT + else + INSTALL_PORTAINER=false + PORTAINER_PORT="9443" + fi +else + INSTALL_DOCKER=false + INSTALL_PORTAINER=false + PORTAINER_PORT="9443" +fi +echo "" + +# ── Ubuntu-spezifische Optionen ──────────────────────────────────────────────── +ENABLE_UNATTENDED=false +DISABLE_SNAP=false +DISABLE_PRO_MOTD=false + +if [[ "$DISTRO" == "ubuntu" ]]; then + echo -e "${CYAN}── Ubuntu-Optionen ──────────────────────────────────────────${NC}" + ask_yn "Automatische Sicherheitsupdates (unattended-upgrades)?" "j" \ + && ENABLE_UNATTENDED=true || true + ask_yn "Snap deaktivieren? (spart Ressourcen im LXC)" "j" \ + && DISABLE_SNAP=true || true + ask_yn "Ubuntu Pro / ESM Werbemeldungen im MOTD deaktivieren?" "j" \ + && DISABLE_PRO_MOTD=true || true + echo "" +fi + +# ── Optionale Pakete ─────────────────────────────────────────────────────────── +echo -e "${CYAN}── Optionale Pakete ─────────────────────────────────────────${NC}" +ask_yn "curl, wget, git (Basis-Tools)" "j" && INSTALL_BASETOOLS=true || INSTALL_BASETOOLS=false +ask_yn "htop, ncdu, net-tools (System-Monitoring)" "j" && INSTALL_MONITORING=true || INSTALL_MONITORING=false +ask_yn "vim + nano (Editoren)" "j" && INSTALL_EDITORS=true || INSTALL_EDITORS=false +ask_yn "cron + logrotate (Automatisierung)" "j" && INSTALL_CRON=true || INSTALL_CRON=false +ask_yn "fail2ban (Brute-Force-Schutz)" "n" && INSTALL_FAIL2BAN=true || INSTALL_FAIL2BAN=false +ask_yn "ufw (Firewall)" "n" && INSTALL_UFW=true || INSTALL_UFW=false +echo "" + +# ══════════════════════════════════════════════════════════════════════════════ +# SCHRITT 5 – SSH +# ══════════════════════════════════════════════════════════════════════════════ +echo -e "${CYAN}╔══ Schritt 5: SSH-Zugriff ══════════════════════════════════════╗${NC}" +echo "" +if ask_yn "🔑 SSH-Root-Login von externen Terminals aktivieren?" "j"; then + ENABLE_ROOT_SSH=true + ask "SSH-Port" "22" SSH_PORT + echo "" + echo -e " ${BOLD}Authentifizierung:${NC}" + echo -e " ${YELLOW}[1]${NC} Nur Passwort" + echo -e " ${YELLOW}[2]${NC} Nur SSH-Key" + echo -e " ${YELLOW}[3]${NC} Passwort + SSH-Key" + echo -ne "${BOLD} Auswahl${NC} [${YELLOW}1${NC}]: " + read -r ssh_auth_sel + case "${ssh_auth_sel:-1}" in + 2) SSH_AUTH="key" ;; + 3) SSH_AUTH="both" ;; + *) SSH_AUTH="password" ;; + esac + SSH_PUBKEY="" + if [[ "$SSH_AUTH" == "key" || "$SSH_AUTH" == "both" ]]; then + echo "" + echo -e " Public Key eingeben (Inhalt von ${YELLOW}~/.ssh/id_rsa.pub${NC}):" + echo -ne " ${BOLD}Key:${NC} " + read -r SSH_PUBKEY + if [[ -z "$SSH_PUBKEY" ]]; then + warn "Kein Key eingegeben – falle auf Passwort zurück." + SSH_AUTH="password" + fi + fi +else + ENABLE_ROOT_SSH=false + SSH_PORT="22" + SSH_AUTH="password" +fi +echo "" + +# ══════════════════════════════════════════════════════════════════════════════ +# ZUSAMMENFASSUNG +# ══════════════════════════════════════════════════════════════════════════════ +echo -e "${BOLD}╔════════════════════════ Zusammenfassung ════════════════════════╗${NC}" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Distribution:" "${DISTRO_LABEL} ${CHOSEN_VER}" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Template:" "$CHOSEN_TEMPLATE" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Container-ID:" "$CTID" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Hostname:" "$HOSTNAME" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Storage:" "$STORAGE" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Disk / RAM / Swap:" "${DISK}GB / ${RAM}MB / ${SWAP}MB" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "CPU / Netzwerk:" "${CORES} Kerne / ${IP_ADDR}" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Zeitzone:" "$TIMEZONE" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Unprivilegiert:" "$([[ $UNPRIVILEGED -eq 1 ]] && echo Ja || echo Nein)" +echo -e "${BOLD}║${NC} ───────────────────────────────────────────────────────── ${BOLD}║${NC}" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Docker:" "$($INSTALL_DOCKER && echo Ja || echo Nein)" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Portainer CE:" "$($INSTALL_PORTAINER && echo "Ja (:${PORTAINER_PORT})" || echo Nein)" +if [[ "$DISTRO" == "ubuntu" ]]; then +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Unattended-Upgrades:" "$($ENABLE_UNATTENDED && echo Ja || echo Nein)" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Snap deaktiviert:" "$($DISABLE_SNAP && echo Ja || echo Nein)" +fi +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Basis-Tools:" "$($INSTALL_BASETOOLS && echo Ja || echo Nein)" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Monitoring-Tools:" "$($INSTALL_MONITORING && echo Ja || echo Nein)" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Editoren:" "$($INSTALL_EDITORS && echo Ja || echo Nein)" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Cron / Logrotate:" "$($INSTALL_CRON && echo Ja || echo Nein)" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "fail2ban:" "$($INSTALL_FAIL2BAN && echo Ja || echo Nein)" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "UFW Firewall:" "$($INSTALL_UFW && echo Ja || echo Nein)" +printf "${BOLD}║${NC} %-28s %-30s ${BOLD}║${NC}\n" "Root-SSH:" "$($ENABLE_ROOT_SSH && echo "Ja (Port ${SSH_PORT}, ${SSH_AUTH})" || echo Nein)" +echo -e "${BOLD}╚═════════════════════════════════════════════════════════════════╝${NC}" +echo "" +ask_yn "Jetzt installieren?" "j" || { echo "Abgebrochen."; exit 0; } + +# ══════════════════════════════════════════════════════════════════════════════ +# INSTALLATION +# ══════════════════════════════════════════════════════════════════════════════ + +# ── Template herunterladen ───────────────────────────────────────────────────── +info "Prüfe Template …" +DOWNLOADED=$(pveam list local 2>/dev/null | awk '{print $1}' | grep -F "$CHOSEN_TEMPLATE" || true) +if [[ -z "$DOWNLOADED" ]]; then + info "Lade Template herunter: ${CHOSEN_TEMPLATE} …" + pveam download local "$CHOSEN_TEMPLATE" + success "Template heruntergeladen." +else + success "Template bereits lokal vorhanden." +fi +TEMPLATE_PATH="local:vztmpl/${CHOSEN_TEMPLATE}" + +# ── LXC erstellen ───────────────────────────────────────────────────────────── +info "Erstelle LXC-Container ${CTID} …" +NET_CONFIG="name=eth0,bridge=${BRIDGE}" +[[ "$IP_ADDR" == "dhcp" ]] && NET_CONFIG+=",ip=dhcp" || { + NET_CONFIG+=",ip=${IP_ADDR}" + [[ -n "$GATEWAY" ]] && NET_CONFIG+=",gw=${GATEWAY}" +} + +FEATURES="keyctl=1,nesting=1" +# Bei Docker + unprivilegiert: nesting zwingend, bei privilegiert nicht nötig aber schadet nicht +$INSTALL_DOCKER && [[ $UNPRIVILEGED -eq 1 ]] && FEATURES="keyctl=1,nesting=1" + +pct create "$CTID" "$TEMPLATE_PATH" \ + --hostname "$HOSTNAME" \ + --password "$PASSWORD" \ + --storage "$STORAGE" \ + --rootfs "${STORAGE}:${DISK}" \ + --memory "$RAM" \ + --swap "$SWAP" \ + --cores "$CORES" \ + --net0 "$NET_CONFIG" \ + --unprivileged "$UNPRIVILEGED" \ + --features "$FEATURES" \ + --onboot 1 --start 0 + +success "Container ${CTID} erstellt." + +info "Starte Container …" +pct start "$CTID" +sleep 6 + +lxc_exec() { pct exec "$CTID" -- bash -c "$*"; } + +# ── Noninteraktiv setzen (Ubuntu) ────────────────────────────────────────────── +[[ "$DISTRO" == "ubuntu" ]] && \ + lxc_exec "echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections 2>/dev/null || true" + +# ── System aktualisieren ─────────────────────────────────────────────────────── +info "System aktualisieren …" +lxc_exec " +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get upgrade -y -qq -o Dpkg::Options::='--force-confold' +apt-get install -y -qq ca-certificates locales +" +success "System aktuell." + +# ── Zeitzone & Locale ────────────────────────────────────────────────────────── +info "Zeitzone & Locale konfigurieren …" +lxc_exec " +export DEBIAN_FRONTEND=noninteractive +ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime +echo '${TIMEZONE}' > /etc/timezone +dpkg-reconfigure -f noninteractive tzdata 2>/dev/null || true +sed -i 's/^# *de_DE.UTF-8/de_DE.UTF-8/' /etc/locale.gen 2>/dev/null || true +sed -i 's/^# *en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen 2>/dev/null || true +locale-gen 2>/dev/null || true +update-locale LANG=de_DE.UTF-8 LC_MESSAGES=en_US.UTF-8 2>/dev/null || true +" +success "Zeitzone: ${TIMEZONE} | Locale: de_DE.UTF-8" + +# ── Ubuntu-spezifisch: Snap / Pro-MOTD / Unattended ─────────────────────────── +if [[ "$DISTRO" == "ubuntu" ]]; then + if $DISABLE_SNAP; then + info "Snap deaktivieren …" + lxc_exec " +systemctl stop snapd 2>/dev/null || true +systemctl disable snapd 2>/dev/null || true +systemctl mask snapd 2>/dev/null || true +apt-get purge -y -qq snapd 2>/dev/null || true +rm -rf /snap /var/snap /var/lib/snapd /var/cache/snapd 2>/dev/null || true +cat > /etc/apt/preferences.d/no-snapd << 'PIN' +Package: snapd +Pin: release a=* +Pin-Priority: -10 +PIN +" 2>/dev/null + success "Snap deaktiviert." + fi + + if $DISABLE_PRO_MOTD; then + info "Ubuntu Pro MOTD deaktivieren …" + lxc_exec " +for f in 10-help-text 50-motd-news 88-esm-announce 91-contract-ua-esm-status 95-hwe-eol; do + [ -f /etc/update-motd.d/\$f ] && chmod -x /etc/update-motd.d/\$f || true +done +command -v pro &>/dev/null && pro config set motd=false 2>/dev/null || true +" 2>/dev/null + success "Ubuntu Pro MOTD deaktiviert." + fi + + if $ENABLE_UNATTENDED; then + info "Unattended-Upgrades konfigurieren …" + lxc_exec " +export DEBIAN_FRONTEND=noninteractive +apt-get install -y -qq unattended-upgrades update-notifier-common +cat > /etc/apt/apt.conf.d/20auto-upgrades << 'AUTO' +APT::Periodic::Update-Package-Lists \"1\"; +APT::Periodic::Unattended-Upgrade \"1\"; +APT::Periodic::AutocleanInterval \"7\"; +APT::Periodic::Download-Upgradeable-Packages \"1\"; +AUTO +systemctl enable unattended-upgrades --now 2>/dev/null || true +" + success "Unattended-Upgrades aktiv." + fi +fi + +# ── Optionale Pakete ─────────────────────────────────────────────────────────── +PKGS=() +$INSTALL_BASETOOLS && PKGS+=(curl wget git) +$INSTALL_MONITORING && PKGS+=(htop ncdu tree net-tools iputils-ping) +$INSTALL_EDITORS && PKGS+=(vim nano) +$INSTALL_CRON && PKGS+=(cron logrotate) +$INSTALL_FAIL2BAN && PKGS+=(fail2ban) +$INSTALL_UFW && PKGS+=(ufw) + +if [[ ${#PKGS[@]} -gt 0 ]]; then + info "Installiere Pakete: ${PKGS[*]} …" + lxc_exec "export DEBIAN_FRONTEND=noninteractive && apt-get install -y -qq ${PKGS[*]}" + success "Pakete installiert." +fi + +# ── fail2ban konfigurieren ───────────────────────────────────────────────────── +if $INSTALL_FAIL2BAN; then + info "fail2ban konfigurieren …" + lxc_exec " +cat > /etc/fail2ban/jail.local << 'F2B' +[DEFAULT] +bantime = 1h +findtime = 10m +maxretry = 5 +backend = systemd + +[sshd] +enabled = true +F2B +systemctl enable fail2ban --now 2>/dev/null || true +" + success "fail2ban aktiv (5 Versuche / 10 Min → 1h Ban)." +fi + +# ── UFW konfigurieren ────────────────────────────────────────────────────────── +if $INSTALL_UFW; then + info "UFW vorkonfigurieren …" + lxc_exec " +ufw default deny incoming +ufw default allow outgoing +ufw allow ${SSH_PORT}/tcp comment 'SSH' +" + success "UFW konfiguriert (deaktiviert – mit 'ufw enable' aktivieren)." +fi + +# ── Docker installieren ──────────────────────────────────────────────────────── +if $INSTALL_DOCKER; then + info "Docker Engine installieren (offizieller Kanal) …" + lxc_exec " +export DEBIAN_FRONTEND=noninteractive +apt-get install -y -qq ca-certificates curl gnupg lsb-release + +install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/${DISTRO}/gpg \ + | gpg --dearmor -o /etc/apt/keyrings/docker.gpg --yes +chmod a+r /etc/apt/keyrings/docker.gpg + +echo \"deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/${DISTRO} \ + \$(. /etc/os-release && echo \"\$VERSION_CODENAME\") stable\" \ + > /etc/apt/sources.list.d/docker.list + +apt-get update -qq +apt-get install -y -qq \ + docker-ce docker-ce-cli containerd.io \ + docker-buildx-plugin docker-compose-plugin + +systemctl enable docker --now +" + success "Docker installiert." + + info "Docker-Funktionstest …" + lxc_exec "docker run --rm hello-world" &>/dev/null \ + && success "Docker läuft korrekt." \ + || warn "Docker-Test fehlgeschlagen – bitte manuell prüfen." +fi + +# ── Portainer installieren ───────────────────────────────────────────────────── +if $INSTALL_PORTAINER; then + info "Portainer CE installieren (Port ${PORTAINER_PORT}) …" + lxc_exec " +docker volume create portainer_data +docker run -d \ + --name portainer \ + --restart always \ + -p 8000:8000 \ + -p ${PORTAINER_PORT}:9443 \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v portainer_data:/data \ + portainer/portainer-ce:latest +" + success "Portainer CE gestartet." +fi + +# ── SSH konfigurieren ────────────────────────────────────────────────────────── +if $ENABLE_ROOT_SSH; then + info "SSH-Server konfigurieren …" + lxc_exec "export DEBIAN_FRONTEND=noninteractive && apt-get install -y -qq openssh-server" + + if [[ "$SSH_CFG_MODE" == "sshd_config_d" ]]; then + # Ubuntu: Drop-in Datei + lxc_exec " +mkdir -p /etc/ssh/sshd_config.d +cat > /etc/ssh/sshd_config.d/99-lxc.conf << SSHEOF +PermitRootLogin yes +Port ${SSH_PORT} +SSHEOF +" + else + # Debian: direkt in sshd_config + lxc_exec " +sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config +grep -q '^PermitRootLogin' /etc/ssh/sshd_config || echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config +sed -i 's/^#*Port .*/Port ${SSH_PORT}/' /etc/ssh/sshd_config +grep -q '^Port ' /etc/ssh/sshd_config || echo 'Port ${SSH_PORT}' >> /etc/ssh/sshd_config +" + fi + + # Auth-Methode + case "$SSH_AUTH" in + password) + lxc_exec " +CFG=${SSH_CFG_MODE/sshd_config_d//etc/ssh/sshd_config.d/99-lxc.conf} +[[ '$SSH_CFG_MODE' == 'sshd_config_d' ]] && FILE=/etc/ssh/sshd_config.d/99-lxc.conf || FILE=/etc/ssh/sshd_config +echo 'PasswordAuthentication yes' >> \$FILE +echo 'PubkeyAuthentication no' >> \$FILE +" ;; + key|both) + lxc_exec " +[[ '$SSH_CFG_MODE' == 'sshd_config_d' ]] && FILE=/etc/ssh/sshd_config.d/99-lxc.conf || FILE=/etc/ssh/sshd_config +[[ '$SSH_AUTH' == 'both' ]] && echo 'PasswordAuthentication yes' >> \$FILE || echo 'PasswordAuthentication no' >> \$FILE +echo 'PubkeyAuthentication yes' >> \$FILE +mkdir -p /root/.ssh && chmod 700 /root/.ssh +echo '${SSH_PUBKEY}' >> /root/.ssh/authorized_keys +chmod 600 /root/.ssh/authorized_keys +" ;; + esac + + lxc_exec "systemctl enable ssh --now && systemctl restart ssh" + + sleep 3 + CT_TEST_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}' || true) + if [[ -n "$CT_TEST_IP" ]] && nc -z -w5 "$CT_TEST_IP" "$SSH_PORT" 2>/dev/null; then + success "SSH erreichbar → ssh root@${CT_TEST_IP}$( [[ "$SSH_PORT" != "22" ]] && echo " -p ${SSH_PORT}" )" + else + warn "SSH-Port noch nicht erreichbar – kurz warten und erneut testen." + fi +fi + +# ── Portainer-Timer zurücksetzen ─────────────────────────────────────────────── +if $INSTALL_PORTAINER; then + info "Portainer-Timer zurücksetzen (Neustart nach Installation) …" + pct exec "$CTID" -- docker restart portainer &>/dev/null \ + && success "Portainer neu gestartet – 5-Minuten-Fenster beginnt jetzt." \ + || warn "Portainer-Neustart fehlgeschlagen – manuell: docker restart portainer" +fi + +# ── Proxmox-Notes setzen ─────────────────────────────────────────────────────── +info "Setze Proxmox-Hinweise …" +sleep 2 +NOTES_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}' || echo "") +[[ -z "$NOTES_IP" ]] && NOTES_IP="n/a (DHCP)" + +SSH_LINE="" +$ENABLE_ROOT_SSH && { + [[ "$SSH_PORT" == "22" ]] \ + && SSH_LINE="ssh root@${NOTES_IP}" \ + || SSH_LINE="ssh root@${NOTES_IP} -p ${SSH_PORT}" +} + +NOTE="[![${DISTRO_LABEL}](${DISTRO_BADGE})](${DISTRO_LINK})" +$INSTALL_DOCKER && \ + NOTE+=" [![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com)" +NOTE+=$'\n\n---\n\n' +NOTE+="**🌐 IP-Adresse:** \`${NOTES_IP}\`"$'\n\n' +$INSTALL_PORTAINER && \ + NOTE+="**🐳 Portainer:** [https://${NOTES_IP}:${PORTAINER_PORT}](https://${NOTES_IP}:${PORTAINER_PORT})"$'\n\n' +$ENABLE_ROOT_SSH && \ + NOTE+="**🔑 SSH:** \`${SSH_LINE}\`"$'\n\n' + +INST="" +$INSTALL_BASETOOLS && INST+="curl wget git " +$INSTALL_MONITORING && INST+="htop ncdu net-tools " +$INSTALL_EDITORS && INST+="vim nano " +$INSTALL_CRON && INST+="cron logrotate " +$INSTALL_FAIL2BAN && INST+="fail2ban " +$INSTALL_UFW && INST+="ufw " +$ENABLE_UNATTENDED && INST+="unattended-upgrades " +[[ -n "$INST" ]] && NOTE+="**📦 Pakete:** \`${INST% }\`"$'\n\n' + +NOTE+="**🕐 Zeitzone:** ${TIMEZONE}"$'\n\n' +NOTE+="---"$'\n\n' +NOTE+="Installiert mit [gitea.vourx.com](https://gitea.vourx.com)" +NOTE+=" | [${DOCS_LABEL}](${DOCS_URL})" +NOTE+=" | [${PKG_LABEL}](${PKG_URL})" + +pvesh set /nodes/$(hostname)/lxc/${CTID}/config --description "${NOTE}" 2>/dev/null \ + && success "Proxmox-Hinweise gesetzt." \ + || warn "Hinweise konnten nicht gesetzt werden." + +# ── Aufräumen ────────────────────────────────────────────────────────────────── +info "System aufräumen …" +lxc_exec "export DEBIAN_FRONTEND=noninteractive && apt-get autoremove -y -qq && apt-get autoclean -qq" +success "Aufgeräumt." + +# ── Abschluss ────────────────────────────────────────────────────────────────── +CONTAINER_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}' || echo "unbekannt") + +echo "" +echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║${NC} ${BOLD}Installation abgeschlossen!${NC}" +echo -e "${BOLD}${GREEN}╠══════════════════════════════════════════════════════════════╣${NC}" +echo -e "${GREEN}║${NC}" +printf "${GREEN}║${NC} %-22s ${BOLD}%s %s${NC}\n" "Distribution:" "$DISTRO_LABEL" "$CHOSEN_VER" +printf "${GREEN}║${NC} %-22s ${BOLD}%s${NC}\n" "Container-ID:" "$CTID" +printf "${GREEN}║${NC} %-22s ${BOLD}%s${NC}\n" "Hostname:" "$HOSTNAME" +printf "${GREEN}║${NC} %-22s ${BOLD}%s${NC}\n" "IP-Adresse:" "$CONTAINER_IP" +printf "${GREEN}║${NC} %-22s ${BOLD}%s${NC}\n" "Zeitzone:" "$TIMEZONE" +echo -e "${GREEN}║${NC}" +$INSTALL_DOCKER && \ + printf "${GREEN}║${NC} %-22s ${BOLD}%s${NC}\n" "Docker:" "installiert & aktiv" +if $INSTALL_PORTAINER; then + printf "${GREEN}║${NC} %-22s ${BOLD}https://%s:%s${NC}\n" "Portainer UI:" "$CONTAINER_IP" "$PORTAINER_PORT" + echo -e "${GREEN}║${NC} ${YELLOW} ⏱ Bitte innerhalb von 5 Min. Admin-Account anlegen!${NC}" +fi +if $ENABLE_ROOT_SSH; then + echo -e "${GREEN}║${NC}" + echo -e "${GREEN}║${NC} ${CYAN}── SSH ─────────────────────────────────────────────────${NC}" + [[ "$SSH_PORT" == "22" ]] \ + && printf "${GREEN}║${NC} ${BOLD}ssh root@%s${NC}\n" "$CONTAINER_IP" \ + || printf "${GREEN}║${NC} ${BOLD}ssh root@%s -p %s${NC}\n" "$CONTAINER_IP" "$SSH_PORT" + printf "${GREEN}║${NC} %-22s ${BOLD}%s${NC}\n" "Auth:" "$SSH_AUTH" +fi +if $INSTALL_UFW; then + echo -e "${GREEN}║${NC}" + echo -e "${GREEN}║${NC} ${YELLOW}🔒 UFW installiert aber noch nicht aktiv.${NC}" + echo -e "${GREEN}║${NC} ${YELLOW} Mit 'ufw enable' im Container aktivieren.${NC}" +fi +echo -e "${GREEN}║${NC}" +echo -e "${GREEN}║${NC} ${YELLOW}⚠ Root-Passwort nach erstem Login bitte ändern!${NC}" +echo -e "${GREEN}║${NC}" +echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" +if $INSTALL_PORTAINER; then + echo -e "\n ${YELLOW}Portainer Timeout? →${NC} ${BOLD}pct exec ${CTID} -- docker restart portainer${NC}" +fi +echo "" \ No newline at end of file