#!/bin/bash set -euo pipefail # gameadm-quadlet - Production Deployment Manager # Podman + systemd/Quadlet Integration für Enterprise Game Servers SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" QUADLET_DIR="/etc/containers/systemd" USER_QUADLET_DIR="$HOME/.config/containers/systemd" GAMEADM_DIR="/etc/gameadm" # Farben für bessere Ausgabe RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Logging-Funktion log() { local level="$1" shift local message="$*" case "$level" in "INFO") echo -e "${GREEN}[gameadm-quadlet]${NC} $message" ;; "WARN") echo -e "${YELLOW}[gameadm-quadlet]${NC} $message" ;; "ERROR") echo -e "${RED}[gameadm-quadlet]${NC} $message" ;; "DEBUG") echo -e "${BLUE}[gameadm-quadlet]${NC} $message" ;; esac } # Hilfe anzeigen show_help() { cat < [game] [options] Befehle: setup [rootless|system] - Quadlet-Umgebung einrichten deploy - Game Server als systemd Service deployen start - Service starten stop - Service stoppen restart - Service neu starten status - Service Status anzeigen logs - Service Logs anzeigen update - Zero-Downtime Update durchführen rollback - Rollback zur vorherigen Version health - Health Check durchführen backup - Backup erstellen enable-autoupdate - Automatische Updates aktivieren disable-autoupdate - Automatische Updates deaktivieren Verfügbare Spiele: minecraft (mc) - Minecraft Server rust - Rust Game Server Beispiele: sudo gameadm-quadlet setup system # System-weite Installation gameadm-quadlet setup rootless # Rootless für aktuellen User gameadm-quadlet deploy minecraft # Minecraft als systemd Service gameadm-quadlet status mc # Status anzeigen gameadm-quadlet update rust # Zero-Downtime Update gameadm-quadlet logs minecraft # Logs anzeigen Features: - Podman + systemd/Quadlet Integration - Rootless Betrieb (empfohlen) - Zero-Downtime Updates - Automatische Container-Updates - Health Checks und Monitoring - Rollback-Mechanismus EOF } # Rootless Setup setup_rootless() { log "INFO" "Richte Rootless Podman + Quadlet ein..." # User Linger aktivieren sudo loginctl enable-linger "$USER" log "INFO" "User Linger aktiviert für: $USER" # Cgroups delegieren if ! grep -q "delegate" /etc/systemd/system/user@.service.d/delegate.conf 2>/dev/null; then sudo mkdir -p /etc/systemd/system/user@.service.d/ sudo tee /etc/systemd/system/user@.service.d/delegate.conf > /dev/null </dev/null || echo "none") echo "$current_image" > "$backup_dir/${game}-image-${timestamp}.backup" # Konfiguration backup cp "/etc/${game}-server.conf" "$backup_dir/${game}-config-${timestamp}.backup" 2>/dev/null || true log "INFO" "Backup erstellt: $backup_dir/${game}-*-${timestamp}.backup" } # Rollback durchführen rollback_game() { local game="$1" local backup_dir="/var/backups/gameadm" log "INFO" "Führe Rollback für $game durch..." # Neuestes Backup finden local latest_image_backup=$(ls -t "$backup_dir/${game}-image-"*.backup 2>/dev/null | head -1) local latest_config_backup=$(ls -t "$backup_dir/${game}-config-"*.backup 2>/dev/null | head -1) if [[ -z "$latest_image_backup" ]]; then log "ERROR" "Kein Image-Backup gefunden für Rollback" return 1 fi # Service stoppen service_operation "stop" "$game" # Konfiguration zurücksetzen if [[ -f "$latest_config_backup" ]]; then cp "$latest_config_backup" "/etc/${game}-server.conf" log "INFO" "Konfiguration zurückgesetzt" fi # Image zurücksetzen (komplexer - erfordert Container-Neustart) local backup_image=$(cat "$latest_image_backup") if [[ "$backup_image" != "none" ]]; then log "INFO" "Setze Image zurück auf: $backup_image" # Container mit altem Image neu erstellen recreate_container_with_image "$game" "$backup_image" fi # Service neu starten service_operation "start" "$game" # Health Check nach Rollback sleep 10 if health_check "$game"; then log "INFO" "✓ Rollback erfolgreich - Service ist gesund" else log "ERROR" "Rollback fehlgeschlagen - manuelle Intervention erforderlich" return 1 fi log "INFO" "Rollback abgeschlossen ✓" } # Health Check health_check() { local game="$1" local max_retries=5 local retry=0 while [[ $retry -lt $max_retries ]]; do # Container läuft? if podman inspect "$game" --format '{{.State.Running}}' 2>/dev/null | grep -q true; then # Port-Check case "$game" in "minecraft") if netstat -tlnp 2>/dev/null | grep -q ":25565"; then return 0 fi ;; "rust") if netstat -tlnp 2>/dev/null | grep -q ":28015"; then return 0 fi ;; esac fi ((retry++)) log "DEBUG" "Health Check Retry $retry/$max_retries für $game" sleep 5 done return 1 } # Container mit spezifischem Image neu erstellen recreate_container_with_image() { local game="$1" local image="$2" log "INFO" "Erstelle Container neu mit Image: $image" # Aktuellen Container stoppen und entfernen podman stop "$game" 2>/dev/null || true podman rm "$game" 2>/dev/null || true # Quadlet neu deployen (wird neues Image verwenden) deploy_game "$game" } # Alte Backups aufräumen cleanup_old_backups() { local game="$1" local backup_dir="/var/backups/gameadm" local keep_backups=5 # Behalte nur die letzten N Backups ls -t "$backup_dir/${game}-"*.backup 2>/dev/null | tail -n +$((keep_backups + 1)) | xargs rm -f 2>/dev/null || true log "DEBUG" "Alte Backups aufgeräumt - behalte $keep_backups neueste" } # Game Image ermitteln get_game_image() { local game="$1" case "$game" in "minecraft") echo "docker.io/itzg/minecraft-server:latest" ;; "rust") echo "docker.io/didstopia/rust-server:latest" ;; esac } # Hauptfunktion main() { if [[ $# -eq 0 ]]; then show_help exit 0 fi local command="$1" shift case "$command" in "help"|"-h"|"--help") show_help ;; "setup") local mode="${1:-rootless}" case "$mode" in "rootless") setup_rootless ;; "system") setup_system ;; *) log "ERROR" "Unbekannter Setup-Modus: $mode"; exit 1 ;; esac ;; "deploy") if [[ $# -eq 0 ]]; then log "ERROR" "Game-Name erforderlich" exit 1 fi deploy_game "$1" "${2:-auto}" ;; "start"|"stop"|"restart"|"status"|"logs"|"enable"|"disable") if [[ $# -eq 0 ]]; then log "ERROR" "Game-Name erforderlich" exit 1 fi service_operation "$command" "$1" ;; "update") if [[ $# -eq 0 ]]; then log "ERROR" "Game-Name erforderlich" exit 1 fi update_game "$1" ;; "rollback") if [[ $# -eq 0 ]]; then log "ERROR" "Game-Name erforderlich" exit 1 fi rollback_game "$1" ;; "health"|"health-check") if [[ $# -eq 0 ]]; then log "ERROR" "Game-Name erforderlich" exit 1 fi if health_check "$1"; then log "INFO" "✓ $1 Service ist gesund" else log "ERROR" "✗ $1 Service ist nicht gesund" exit 1 fi ;; "backup") if [[ $# -eq 0 ]]; then log "ERROR" "Game-Name erforderlich" exit 1 fi create_backup "$1" ;; *) log "ERROR" "Unbekannter Befehl: $command" show_help exit 1 ;; esac } # Hauptprogramm ausführen main "$@"