gameadm/bin/gameadm-quadlet

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 "$@"