487 lines
14 KiB
Bash
Executable File
487 lines
14 KiB
Bash
Executable File
#!/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 <<EOF
|
|
gameadm-quadlet - Production Deployment Manager
|
|
|
|
Verwendung:
|
|
gameadm-quadlet <command> [game] [options]
|
|
|
|
Befehle:
|
|
setup [rootless|system] - Quadlet-Umgebung einrichten
|
|
deploy <game> - Game Server als systemd Service deployen
|
|
start <game> - Service starten
|
|
stop <game> - Service stoppen
|
|
restart <game> - Service neu starten
|
|
status <game> - Service Status anzeigen
|
|
logs <game> - Service Logs anzeigen
|
|
update <game> - Zero-Downtime Update durchführen
|
|
rollback <game> - Rollback zur vorherigen Version
|
|
health <game> - Health Check durchführen
|
|
backup <game> - Backup erstellen
|
|
enable-autoupdate <game> - Automatische Updates aktivieren
|
|
disable-autoupdate <game> - 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 <<EOF
|
|
[Service]
|
|
Delegate=yes
|
|
EOF
|
|
sudo systemctl daemon-reload
|
|
log "INFO" "Cgroups Delegation konfiguriert"
|
|
fi
|
|
|
|
# User Quadlet Directory erstellen
|
|
mkdir -p "$USER_QUADLET_DIR"
|
|
log "INFO" "User Quadlet Directory: $USER_QUADLET_DIR"
|
|
|
|
# User systemd reload
|
|
systemctl --user daemon-reload
|
|
log "INFO" "User systemd reloaded"
|
|
|
|
log "INFO" "Rootless Setup abgeschlossen ✓"
|
|
}
|
|
|
|
# System Setup
|
|
setup_system() {
|
|
log "INFO" "Richte System-weites Quadlet ein..."
|
|
|
|
# Prüfe Root-Berechtigung
|
|
if [[ $EUID -ne 0 ]]; then
|
|
log "ERROR" "System Setup muss als root ausgeführt werden"
|
|
exit 1
|
|
fi
|
|
|
|
# System Quadlet Directory erstellen
|
|
mkdir -p "$QUADLET_DIR"
|
|
log "INFO" "System Quadlet Directory: $QUADLET_DIR"
|
|
|
|
# systemd reload
|
|
systemctl daemon-reload
|
|
log "INFO" "System systemd reloaded"
|
|
|
|
log "INFO" "System Setup abgeschlossen ✓"
|
|
}
|
|
|
|
# Game deployen
|
|
deploy_game() {
|
|
local game="$1"
|
|
local use_rootless="${2:-auto}"
|
|
|
|
# Game normalisieren
|
|
case "$game" in
|
|
"mc"|"minecraft") game="minecraft" ;;
|
|
"rust") game="rust" ;;
|
|
*) log "ERROR" "Unbekanntes Spiel: $game"; exit 1 ;;
|
|
esac
|
|
|
|
log "INFO" "Deploye $game als systemd Service..."
|
|
|
|
# Prüfe ob gameadm Konfiguration existiert
|
|
local config_file="/etc/${game}-server.conf"
|
|
if [[ ! -f "$config_file" ]]; then
|
|
log "ERROR" "Konfiguration nicht gefunden: $config_file"
|
|
log "INFO" "Führe erst aus: gameadm install $game"
|
|
exit 1
|
|
fi
|
|
|
|
# Bestimme Zielverzeichnis
|
|
local target_dir
|
|
if [[ "$use_rootless" == "rootless" ]] || [[ $EUID -ne 0 && "$use_rootless" == "auto" ]]; then
|
|
target_dir="$USER_QUADLET_DIR"
|
|
log "INFO" "Verwende Rootless Deployment"
|
|
else
|
|
target_dir="$QUADLET_DIR"
|
|
log "INFO" "Verwende System Deployment"
|
|
fi
|
|
|
|
# Kopiere Quadlet-Datei
|
|
local source_file="$GAMEADM_DIR/production/quadlet/${game}.container"
|
|
local target_file="$target_dir/${game}.container"
|
|
|
|
if [[ ! -f "$source_file" ]]; then
|
|
log "ERROR" "Quadlet-Template nicht gefunden: $source_file"
|
|
exit 1
|
|
fi
|
|
|
|
cp "$source_file" "$target_file"
|
|
log "INFO" "Quadlet-Datei installiert: $target_file"
|
|
|
|
# systemd reload
|
|
if [[ "$target_dir" == "$USER_QUADLET_DIR" ]]; then
|
|
systemctl --user daemon-reload
|
|
log "INFO" "User systemd reloaded"
|
|
else
|
|
systemctl daemon-reload
|
|
log "INFO" "System systemd reloaded"
|
|
fi
|
|
|
|
log "INFO" "$game Service deployed ✓"
|
|
log "INFO" "Starten mit: gameadm-quadlet start $game"
|
|
}
|
|
|
|
# Service-Operationen
|
|
service_operation() {
|
|
local operation="$1"
|
|
local game="$2"
|
|
|
|
# Game normalisieren
|
|
case "$game" in
|
|
"mc"|"minecraft") game="minecraft" ;;
|
|
"rust") game="rust" ;;
|
|
*) log "ERROR" "Unbekanntes Spiel: $game"; exit 1 ;;
|
|
esac
|
|
|
|
# Bestimme systemctl Kontext
|
|
local systemctl_cmd="systemctl"
|
|
if [[ -f "$USER_QUADLET_DIR/${game}.container" ]]; then
|
|
systemctl_cmd="systemctl --user"
|
|
fi
|
|
|
|
case "$operation" in
|
|
"start")
|
|
log "INFO" "Starte $game Service..."
|
|
$systemctl_cmd start "$game"
|
|
;;
|
|
"stop")
|
|
log "INFO" "Stoppe $game Service..."
|
|
$systemctl_cmd stop "$game"
|
|
;;
|
|
"restart")
|
|
log "INFO" "Starte $game Service neu..."
|
|
$systemctl_cmd restart "$game"
|
|
;;
|
|
"status")
|
|
$systemctl_cmd status "$game" --no-pager
|
|
;;
|
|
"logs")
|
|
$systemctl_cmd logs -f "$game"
|
|
;;
|
|
"enable")
|
|
$systemctl_cmd enable "$game"
|
|
log "INFO" "$game Service beim Boot aktiviert"
|
|
;;
|
|
"disable")
|
|
$systemctl_cmd disable "$game"
|
|
log "INFO" "$game Service beim Boot deaktiviert"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Zero-Downtime Update
|
|
update_game() {
|
|
local game="$1"
|
|
|
|
log "INFO" "Führe Zero-Downtime Update für $game durch..."
|
|
|
|
# Pre-Update Backup
|
|
create_backup "$game"
|
|
|
|
# Health Check vor Update
|
|
if ! health_check "$game"; then
|
|
log "WARN" "Service nicht gesund vor Update - Update wird trotzdem fortgesetzt"
|
|
fi
|
|
|
|
# Neue Images pullen
|
|
local image=$(get_game_image "$game")
|
|
log "INFO" "Pulling Image: $image"
|
|
podman pull "$image"
|
|
|
|
# Auto-Update ausführen (graceful restart)
|
|
log "INFO" "Führe Auto-Update durch..."
|
|
podman auto-update
|
|
|
|
# Post-Update Health Check
|
|
log "INFO" "Warte auf Service-Start..."
|
|
sleep 10
|
|
|
|
if health_check "$game"; then
|
|
log "INFO" "✓ Update erfolgreich - Service ist gesund"
|
|
cleanup_old_backups "$game"
|
|
else
|
|
log "ERROR" "Update fehlgeschlagen - Service nicht gesund"
|
|
log "WARN" "Automatischer Rollback verfügbar mit: gameadm-quadlet rollback $game"
|
|
return 1
|
|
fi
|
|
|
|
log "INFO" "Zero-Downtime Update abgeschlossen ✓"
|
|
}
|
|
|
|
# Backup erstellen
|
|
create_backup() {
|
|
local game="$1"
|
|
local backup_dir="/var/backups/gameadm"
|
|
local timestamp=$(date +%Y%m%d-%H%M%S)
|
|
|
|
mkdir -p "$backup_dir"
|
|
|
|
# Container Image Info speichern
|
|
local current_image=$(podman inspect "$game" --format '{{.Image}}' 2>/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 "$@"
|