mirror of
https://github.com/uprightbass360/AzerothCore-RealmMaster.git
synced 2026-01-13 09:07:20 +00:00
feat: comprehensive module system and database management improvements
This commit introduces major enhancements to the module installation system, database management, and configuration handling for AzerothCore deployments. ## Module System Improvements ### Module SQL Staging & Installation - Refactor module SQL staging to properly handle AzerothCore's sql/ directory structure - Fix SQL staging path to use correct AzerothCore format (sql/custom/db_*/*) - Implement conditional module database importing based on enabled modules - Add support for both cpp-modules and lua-scripts module types - Handle rsync exit code 23 (permission warnings) gracefully during deployment ### Module Manifest & Automation - Add automated module manifest generation via GitHub Actions workflow - Implement Python-based module manifest updater with comprehensive validation - Add module dependency tracking and SQL file discovery - Support for blocked modules and module metadata management ## Database Management Enhancements ### Database Import System - Add db-guard container for continuous database health monitoring and verification - Implement conditional database import that skips when databases are current - Add backup restoration and SQL staging coordination - Support for Playerbots database (4th database) in all import operations - Add comprehensive database health checking and status reporting ### Database Configuration - Implement 10 new dbimport.conf settings from environment variables: - Database.Reconnect.Seconds/Attempts for connection reliability - Updates.AllowedModules for module auto-update control - Updates.Redundancy for data integrity checks - Worker/Synch thread settings for all three core databases - Auto-apply dbimport.conf settings via auto-post-install.sh - Add environment variable injection for db-import and db-guard containers ### Backup & Recovery - Fix backup scheduler to prevent immediate execution on container startup - Add backup status monitoring script with detailed reporting - Implement backup import/export utilities - Add database verification scripts for SQL update tracking ## User Import Directory - Add new import/ directory for user-provided database files and configurations - Support for custom SQL files, configuration overrides, and example templates - Automatic import of user-provided databases and configs during initialization - Documentation and examples for custom database imports ## Configuration & Environment - Eliminate CLIENT_DATA_VERSION warning by adding default value syntax - Improve CLIENT_DATA_VERSION documentation in .env.template - Add comprehensive database import settings to .env and .env.template - Update setup.sh to handle new configuration variables with proper defaults ## Monitoring & Debugging - Add status dashboard with Go-based terminal UI (statusdash.go) - Implement JSON status output (statusjson.sh) for programmatic access - Add comprehensive database health check script - Add repair-storage-permissions.sh utility for permission issues ## Testing & Documentation - Add Phase 1 integration test suite for module installation verification - Add comprehensive documentation for: - Database management (DATABASE_MANAGEMENT.md) - Module SQL analysis (AZEROTHCORE_MODULE_SQL_ANALYSIS.md) - Implementation mapping (IMPLEMENTATION_MAP.md) - SQL staging comparison and path coverage - Module assets and DBC file requirements - Update SCRIPTS.md, ADVANCED.md, and troubleshooting documentation - Update references from database-import/ to import/ directory ## Breaking Changes - Renamed database-import/ directory to import/ for clarity - Module SQL files now staged to AzerothCore-compatible paths - db-guard container now required for proper database lifecycle management ## Bug Fixes - Fix module SQL staging directory structure for AzerothCore compatibility - Handle rsync exit code 23 gracefully during deployments - Prevent backup from running immediately on container startup - Correct SQL staging paths for proper module installation
This commit is contained in:
@@ -100,7 +100,14 @@ else
|
||||
|
||||
# Skip core config files (already handled)
|
||||
case "$filename" in
|
||||
authserver.conf|worldserver.conf|dbimport.conf)
|
||||
authserver.conf|worldserver.conf)
|
||||
continue
|
||||
;;
|
||||
dbimport.conf)
|
||||
if [ ! -f "$conffile" ] || grep -q "Updates.ExceptionShutdownDelay" "$conffile"; then
|
||||
echo " 📝 Creating/refreshing $filename from $(basename "$file")"
|
||||
cp "$file" "$conffile"
|
||||
fi
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
@@ -140,6 +147,28 @@ else
|
||||
sed -i "s|^LoginDatabaseInfo *=.*|LoginDatabaseInfo = \"${MYSQL_HOST};${MYSQL_PORT};${MYSQL_USER};${MYSQL_ROOT_PASSWORD};${DB_AUTH_NAME}\"|" /azerothcore/config/worldserver.conf || true
|
||||
sed -i "s|^WorldDatabaseInfo *=.*|WorldDatabaseInfo = \"${MYSQL_HOST};${MYSQL_PORT};${MYSQL_USER};${MYSQL_ROOT_PASSWORD};${DB_WORLD_NAME}\"|" /azerothcore/config/worldserver.conf || true
|
||||
sed -i "s|^CharacterDatabaseInfo *=.*|CharacterDatabaseInfo = \"${MYSQL_HOST};${MYSQL_PORT};${MYSQL_USER};${MYSQL_ROOT_PASSWORD};${DB_CHARACTERS_NAME}\"|" /azerothcore/config/worldserver.conf || true
|
||||
if [ -f "/azerothcore/config/dbimport.conf" ]; then
|
||||
sed -i "s|^LoginDatabaseInfo *=.*|LoginDatabaseInfo = \"${MYSQL_HOST};${MYSQL_PORT};${MYSQL_USER};${MYSQL_ROOT_PASSWORD};${DB_AUTH_NAME}\"|" /azerothcore/config/dbimport.conf || true
|
||||
sed -i "s|^WorldDatabaseInfo *=.*|WorldDatabaseInfo = \"${MYSQL_HOST};${MYSQL_PORT};${MYSQL_USER};${MYSQL_ROOT_PASSWORD};${DB_WORLD_NAME}\"|" /azerothcore/config/dbimport.conf || true
|
||||
sed -i "s|^CharacterDatabaseInfo *=.*|CharacterDatabaseInfo = \"${MYSQL_HOST};${MYSQL_PORT};${MYSQL_USER};${MYSQL_ROOT_PASSWORD};${DB_CHARACTERS_NAME}\"|" /azerothcore/config/dbimport.conf || true
|
||||
sed -i "s|^PlayerbotsDatabaseInfo *=.*|PlayerbotsDatabaseInfo = \"${MYSQL_HOST};${MYSQL_PORT};${MYSQL_USER};${MYSQL_ROOT_PASSWORD};${DB_PLAYERBOTS_NAME}\"|" /azerothcore/config/dbimport.conf || true
|
||||
sed -i "s|^MySQLExecutable *=.*|MySQLExecutable = \"/usr/bin/mysql\"|" /azerothcore/config/dbimport.conf || true
|
||||
sed -i "s|^TempDir *=.*|TempDir = \"/azerothcore/env/dist/temp\"|" /azerothcore/config/dbimport.conf || true
|
||||
# Database reconnection settings
|
||||
sed -i "s|^Database\.Reconnect\.Seconds *=.*|Database.Reconnect.Seconds = ${DB_RECONNECT_SECONDS:-5}|" /azerothcore/config/dbimport.conf || true
|
||||
sed -i "s|^Database\.Reconnect\.Attempts *=.*|Database.Reconnect.Attempts = ${DB_RECONNECT_ATTEMPTS:-5}|" /azerothcore/config/dbimport.conf || true
|
||||
# Update settings
|
||||
sed -i "s|^Updates\.AllowedModules *=.*|Updates.AllowedModules = \"${DB_UPDATES_ALLOWED_MODULES:-all}\"|" /azerothcore/config/dbimport.conf || true
|
||||
sed -i "s|^Updates\.Redundancy *=.*|Updates.Redundancy = ${DB_UPDATES_REDUNDANCY:-1}|" /azerothcore/config/dbimport.conf || true
|
||||
# Worker thread settings
|
||||
sed -i "s|^LoginDatabase\.WorkerThreads *=.*|LoginDatabase.WorkerThreads = ${DB_LOGIN_WORKER_THREADS:-1}|" /azerothcore/config/dbimport.conf || true
|
||||
sed -i "s|^WorldDatabase\.WorkerThreads *=.*|WorldDatabase.WorkerThreads = ${DB_WORLD_WORKER_THREADS:-1}|" /azerothcore/config/dbimport.conf || true
|
||||
sed -i "s|^CharacterDatabase\.WorkerThreads *=.*|CharacterDatabase.WorkerThreads = ${DB_CHARACTER_WORKER_THREADS:-1}|" /azerothcore/config/dbimport.conf || true
|
||||
# Synch thread settings
|
||||
sed -i "s|^LoginDatabase\.SynchThreads *=.*|LoginDatabase.SynchThreads = ${DB_LOGIN_SYNCH_THREADS:-1}|" /azerothcore/config/dbimport.conf || true
|
||||
sed -i "s|^WorldDatabase\.SynchThreads *=.*|WorldDatabase.SynchThreads = ${DB_WORLD_SYNCH_THREADS:-1}|" /azerothcore/config/dbimport.conf || true
|
||||
sed -i "s|^CharacterDatabase\.SynchThreads *=.*|CharacterDatabase.SynchThreads = ${DB_CHARACTER_SYNCH_THREADS:-1}|" /azerothcore/config/dbimport.conf || true
|
||||
fi
|
||||
update_playerbots_conf /azerothcore/config/modules/playerbots.conf
|
||||
update_playerbots_conf /azerothcore/config/modules/playerbots.conf.dist
|
||||
|
||||
|
||||
@@ -200,8 +200,9 @@ cleanup_old() {
|
||||
|
||||
log "Backup scheduler starting: interval(${BACKUP_INTERVAL_MINUTES}m), daily($RETENTION_DAYS d at ${DAILY_TIME}:00)"
|
||||
|
||||
# Initialize last backup time
|
||||
last_backup=0
|
||||
# Initialize last backup time to current time to prevent immediate backup on startup
|
||||
last_backup=$(date +%s)
|
||||
log "ℹ️ First backup will run in ${BACKUP_INTERVAL_MINUTES} minutes"
|
||||
|
||||
while true; do
|
||||
current_time=$(date +%s)
|
||||
|
||||
421
scripts/bash/backup-status.sh
Executable file
421
scripts/bash/backup-status.sh
Executable file
@@ -0,0 +1,421 @@
|
||||
#!/bin/bash
|
||||
# Backup Status Dashboard
|
||||
# Displays comprehensive backup system status and statistics
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
# Colors
|
||||
BLUE='\033[0;34m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Icons
|
||||
ICON_BACKUP="📦"
|
||||
ICON_TIME="🕐"
|
||||
ICON_SIZE="💾"
|
||||
ICON_CHART="📊"
|
||||
ICON_SUCCESS="✅"
|
||||
ICON_WARNING="⚠️"
|
||||
ICON_SCHEDULE="📅"
|
||||
|
||||
# Default values
|
||||
SHOW_DETAILS=0
|
||||
SHOW_TRENDS=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./backup-status.sh [options]
|
||||
|
||||
Display backup system status and statistics.
|
||||
|
||||
Options:
|
||||
-d, --details Show detailed backup listing
|
||||
-t, --trends Show size trends over time
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
./backup-status.sh
|
||||
./backup-status.sh --details
|
||||
./backup-status.sh --details --trends
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-d|--details) SHOW_DETAILS=1; shift;;
|
||||
-t|--trends) SHOW_TRENDS=1; shift;;
|
||||
-h|--help) usage; exit 0;;
|
||||
*) echo "Unknown option: $1"; usage; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Load environment
|
||||
if [ -f "$PROJECT_ROOT/.env" ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$PROJECT_ROOT/.env"
|
||||
set +a
|
||||
fi
|
||||
|
||||
BACKUP_PATH="${BACKUP_PATH:-$PROJECT_ROOT/storage/backups}"
|
||||
BACKUP_INTERVAL_MINUTES="${BACKUP_INTERVAL_MINUTES:-60}"
|
||||
BACKUP_RETENTION_HOURS="${BACKUP_RETENTION_HOURS:-6}"
|
||||
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-3}"
|
||||
BACKUP_DAILY_TIME="${BACKUP_DAILY_TIME:-09}"
|
||||
|
||||
# Format bytes to human readable
|
||||
format_bytes() {
|
||||
local bytes=$1
|
||||
if [ "$bytes" -lt 1024 ]; then
|
||||
echo "${bytes}B"
|
||||
elif [ "$bytes" -lt 1048576 ]; then
|
||||
echo "$(awk "BEGIN {printf \"%.1f\", $bytes/1024}")KB"
|
||||
elif [ "$bytes" -lt 1073741824 ]; then
|
||||
echo "$(awk "BEGIN {printf \"%.1f\", $bytes/1048576}")MB"
|
||||
else
|
||||
echo "$(awk "BEGIN {printf \"%.2f\", $bytes/1073741824}")GB"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get directory size
|
||||
get_dir_size() {
|
||||
local dir="$1"
|
||||
if [ -d "$dir" ]; then
|
||||
du -sb "$dir" 2>/dev/null | cut -f1
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Count backups in directory
|
||||
count_backups() {
|
||||
local dir="$1"
|
||||
if [ -d "$dir" ]; then
|
||||
find "$dir" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get latest backup timestamp
|
||||
get_latest_backup() {
|
||||
local dir="$1"
|
||||
if [ -d "$dir" ]; then
|
||||
ls -1t "$dir" 2>/dev/null | head -n1 || echo ""
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# Parse timestamp from backup directory name
|
||||
parse_timestamp() {
|
||||
local backup_name="$1"
|
||||
# Format: YYYYMMDD_HHMMSS or ExportBackup_YYYYMMDD_HHMMSS
|
||||
local timestamp
|
||||
if [[ "$backup_name" =~ ([0-9]{8})_([0-9]{6}) ]]; then
|
||||
timestamp="${BASH_REMATCH[1]}_${BASH_REMATCH[2]}"
|
||||
echo "$timestamp"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# Calculate time ago from timestamp
|
||||
time_ago() {
|
||||
local timestamp="$1"
|
||||
if [ -z "$timestamp" ]; then
|
||||
echo "Unknown"
|
||||
return
|
||||
fi
|
||||
|
||||
# Parse timestamp: YYYYMMDD_HHMMSS
|
||||
local year="${timestamp:0:4}"
|
||||
local month="${timestamp:4:2}"
|
||||
local day="${timestamp:6:2}"
|
||||
local hour="${timestamp:9:2}"
|
||||
local minute="${timestamp:11:2}"
|
||||
local second="${timestamp:13:2}"
|
||||
|
||||
local backup_epoch
|
||||
backup_epoch=$(date -d "$year-$month-$day $hour:$minute:$second" +%s 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$backup_epoch" = "0" ]; then
|
||||
echo "Unknown"
|
||||
return
|
||||
fi
|
||||
|
||||
local now_epoch
|
||||
now_epoch=$(date +%s)
|
||||
local diff=$((now_epoch - backup_epoch))
|
||||
|
||||
if [ "$diff" -lt 60 ]; then
|
||||
echo "${diff} seconds ago"
|
||||
elif [ "$diff" -lt 3600 ]; then
|
||||
local minutes=$((diff / 60))
|
||||
echo "${minutes} minute(s) ago"
|
||||
elif [ "$diff" -lt 86400 ]; then
|
||||
local hours=$((diff / 3600))
|
||||
echo "${hours} hour(s) ago"
|
||||
else
|
||||
local days=$((diff / 86400))
|
||||
echo "${days} day(s) ago"
|
||||
fi
|
||||
}
|
||||
|
||||
# Calculate next scheduled backup
|
||||
next_backup_time() {
|
||||
local interval_minutes="$1"
|
||||
local now_epoch
|
||||
now_epoch=$(date +%s)
|
||||
|
||||
local next_epoch=$((now_epoch + (interval_minutes * 60)))
|
||||
local in_minutes=$(((next_epoch - now_epoch) / 60))
|
||||
|
||||
if [ "$in_minutes" -lt 60 ]; then
|
||||
echo "in ${in_minutes} minute(s)"
|
||||
else
|
||||
local in_hours=$((in_minutes / 60))
|
||||
local remaining_minutes=$((in_minutes % 60))
|
||||
echo "in ${in_hours} hour(s) ${remaining_minutes} minute(s)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Calculate next daily backup
|
||||
next_daily_backup() {
|
||||
local daily_hour="$1"
|
||||
local now_epoch
|
||||
now_epoch=$(date +%s)
|
||||
|
||||
local today_backup_epoch
|
||||
today_backup_epoch=$(date -d "today ${daily_hour}:00:00" +%s)
|
||||
|
||||
local next_epoch
|
||||
if [ "$now_epoch" -lt "$today_backup_epoch" ]; then
|
||||
next_epoch=$today_backup_epoch
|
||||
else
|
||||
next_epoch=$(date -d "tomorrow ${daily_hour}:00:00" +%s)
|
||||
fi
|
||||
|
||||
local diff=$((next_epoch - now_epoch))
|
||||
local hours=$((diff / 3600))
|
||||
local minutes=$(((diff % 3600) / 60))
|
||||
|
||||
echo "in ${hours} hour(s) ${minutes} minute(s)"
|
||||
}
|
||||
|
||||
# Show backup tier status
|
||||
show_backup_tier() {
|
||||
local tier_name="$1"
|
||||
local tier_dir="$2"
|
||||
local retention="$3"
|
||||
|
||||
if [ ! -d "$tier_dir" ]; then
|
||||
printf " ${ICON_WARNING} ${YELLOW}%s:${NC} No backups found\n" "$tier_name"
|
||||
return
|
||||
fi
|
||||
|
||||
local count size latest
|
||||
count=$(count_backups "$tier_dir")
|
||||
size=$(get_dir_size "$tier_dir")
|
||||
latest=$(get_latest_backup "$tier_dir")
|
||||
|
||||
if [ "$count" = "0" ]; then
|
||||
printf " ${ICON_WARNING} ${YELLOW}%s:${NC} No backups found\n" "$tier_name"
|
||||
return
|
||||
fi
|
||||
|
||||
local latest_timestamp
|
||||
latest_timestamp=$(parse_timestamp "$latest")
|
||||
local ago
|
||||
ago=$(time_ago "$latest_timestamp")
|
||||
|
||||
printf " ${GREEN}${ICON_SUCCESS} %s:${NC} %s backup(s), %s total\n" "$tier_name" "$count" "$(format_bytes "$size")"
|
||||
printf " ${ICON_TIME} Latest: %s (%s)\n" "$latest" "$ago"
|
||||
printf " ${ICON_SCHEDULE} Retention: %s\n" "$retention"
|
||||
|
||||
if [ "$SHOW_DETAILS" = "1" ]; then
|
||||
printf " ${ICON_BACKUP} Available backups:\n"
|
||||
local backup_list
|
||||
backup_list=$(ls -1t "$tier_dir" 2>/dev/null || true)
|
||||
while IFS= read -r backup; do
|
||||
if [ -n "$backup" ]; then
|
||||
local backup_size
|
||||
backup_size=$(get_dir_size "$tier_dir/$backup")
|
||||
local backup_timestamp
|
||||
backup_timestamp=$(parse_timestamp "$backup")
|
||||
local backup_ago
|
||||
backup_ago=$(time_ago "$backup_timestamp")
|
||||
printf " - %s: %s (%s)\n" "$backup" "$(format_bytes "$backup_size")" "$backup_ago"
|
||||
fi
|
||||
done <<< "$backup_list"
|
||||
fi
|
||||
}
|
||||
|
||||
# Show size trends
|
||||
show_trends() {
|
||||
printf "${BOLD}${ICON_CHART} Backup Size Trends${NC}\n"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
local daily_dir="$BACKUP_PATH/daily"
|
||||
if [ ! -d "$daily_dir" ]; then
|
||||
printf " ${ICON_WARNING} No daily backups found for trend analysis\n\n"
|
||||
return
|
||||
fi
|
||||
|
||||
# Get last 7 daily backups
|
||||
local backup_list
|
||||
backup_list=$(ls -1t "$daily_dir" 2>/dev/null | head -7 | tac)
|
||||
|
||||
if [ -z "$backup_list" ]; then
|
||||
printf " ${ICON_WARNING} Not enough backups for trend analysis\n\n"
|
||||
return
|
||||
fi
|
||||
|
||||
# Find max size for scaling
|
||||
local max_size=0
|
||||
while IFS= read -r backup; do
|
||||
if [ -n "$backup" ]; then
|
||||
local size
|
||||
size=$(get_dir_size "$daily_dir/$backup")
|
||||
if [ "$size" -gt "$max_size" ]; then
|
||||
max_size=$size
|
||||
fi
|
||||
fi
|
||||
done <<< "$backup_list"
|
||||
|
||||
# Display trend chart
|
||||
while IFS= read -r backup; do
|
||||
if [ -n "$backup" ]; then
|
||||
local size
|
||||
size=$(get_dir_size "$daily_dir/$backup")
|
||||
local timestamp
|
||||
timestamp=$(parse_timestamp "$backup")
|
||||
local date_str="${timestamp:0:4}-${timestamp:4:2}-${timestamp:6:2}"
|
||||
|
||||
# Calculate bar length (max 30 chars)
|
||||
local bar_length=0
|
||||
if [ "$max_size" -gt 0 ]; then
|
||||
bar_length=$((size * 30 / max_size))
|
||||
fi
|
||||
|
||||
# Create bar
|
||||
local bar=""
|
||||
for ((i=0; i<bar_length; i++)); do
|
||||
bar+="█"
|
||||
done
|
||||
for ((i=bar_length; i<30; i++)); do
|
||||
bar+="░"
|
||||
done
|
||||
|
||||
printf " %s: %s %s\n" "$date_str" "$(format_bytes "$size" | awk '{printf "%-8s", $0}')" "$bar"
|
||||
fi
|
||||
done <<< "$backup_list"
|
||||
echo
|
||||
}
|
||||
|
||||
# Main status display
|
||||
main() {
|
||||
echo
|
||||
printf "${BOLD}${BLUE}${ICON_BACKUP} AZEROTHCORE BACKUP STATUS${NC}\n"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo
|
||||
|
||||
# Check if backup directory exists
|
||||
if [ ! -d "$BACKUP_PATH" ]; then
|
||||
printf "${RED}${ICON_WARNING} Backup directory not found: %s${NC}\n\n" "$BACKUP_PATH"
|
||||
printf "Backup system may not be initialized yet.\n\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show current backup tiers
|
||||
printf "${BOLD}${ICON_BACKUP} Backup Tiers${NC}\n"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
show_backup_tier "Hourly Backups" "$BACKUP_PATH/hourly" "${BACKUP_RETENTION_HOURS} hours"
|
||||
show_backup_tier "Daily Backups" "$BACKUP_PATH/daily" "${BACKUP_RETENTION_DAYS} days"
|
||||
|
||||
# Check for manual backups
|
||||
local manual_count=0
|
||||
local manual_size=0
|
||||
if [ -d "$PROJECT_ROOT/manual-backups" ]; then
|
||||
manual_count=$(count_backups "$PROJECT_ROOT/manual-backups")
|
||||
manual_size=$(get_dir_size "$PROJECT_ROOT/manual-backups")
|
||||
fi
|
||||
|
||||
# Also check for export backups in main backup dir
|
||||
local export_count=0
|
||||
if [ -d "$BACKUP_PATH" ]; then
|
||||
export_count=$(find "$BACKUP_PATH" -maxdepth 1 -type d -name "ExportBackup_*" 2>/dev/null | wc -l)
|
||||
if [ "$export_count" -gt 0 ]; then
|
||||
local export_size=0
|
||||
while IFS= read -r export_dir; do
|
||||
if [ -n "$export_dir" ]; then
|
||||
local size
|
||||
size=$(get_dir_size "$export_dir")
|
||||
export_size=$((export_size + size))
|
||||
fi
|
||||
done < <(find "$BACKUP_PATH" -maxdepth 1 -type d -name "ExportBackup_*" 2>/dev/null)
|
||||
manual_size=$((manual_size + export_size))
|
||||
manual_count=$((manual_count + export_count))
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$manual_count" -gt 0 ]; then
|
||||
printf " ${GREEN}${ICON_SUCCESS} Manual/Export Backups:${NC} %s backup(s), %s total\n" "$manual_count" "$(format_bytes "$manual_size")"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
# Show next scheduled backups
|
||||
printf "${BOLD}${ICON_SCHEDULE} Backup Schedule${NC}\n"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
printf " ${ICON_TIME} Hourly interval: every %s minutes\n" "$BACKUP_INTERVAL_MINUTES"
|
||||
printf " ${ICON_TIME} Next hourly backup: %s\n" "$(next_backup_time "$BACKUP_INTERVAL_MINUTES")"
|
||||
printf " ${ICON_TIME} Daily backup time: %s:00\n" "$BACKUP_DAILY_TIME"
|
||||
printf " ${ICON_TIME} Next daily backup: %s\n" "$(next_daily_backup "$BACKUP_DAILY_TIME")"
|
||||
echo
|
||||
|
||||
# Calculate total storage
|
||||
local total_size=0
|
||||
for tier_dir in "$BACKUP_PATH/hourly" "$BACKUP_PATH/daily"; do
|
||||
if [ -d "$tier_dir" ]; then
|
||||
local size
|
||||
size=$(get_dir_size "$tier_dir")
|
||||
total_size=$((total_size + size))
|
||||
fi
|
||||
done
|
||||
total_size=$((total_size + manual_size))
|
||||
|
||||
printf "${BOLD}${ICON_SIZE} Total Backup Storage: %s${NC}\n" "$(format_bytes "$total_size")"
|
||||
echo
|
||||
|
||||
# Show trends if requested
|
||||
if [ "$SHOW_TRENDS" = "1" ]; then
|
||||
show_trends
|
||||
fi
|
||||
|
||||
# Show backup configuration
|
||||
if [ "$SHOW_DETAILS" = "1" ]; then
|
||||
printf "${BOLD}⚙️ Backup Configuration${NC}\n"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
printf " Backup directory: %s\n" "$BACKUP_PATH"
|
||||
printf " Hourly retention: %s hours\n" "$BACKUP_RETENTION_HOURS"
|
||||
printf " Daily retention: %s days\n" "$BACKUP_RETENTION_DAYS"
|
||||
printf " Interval: every %s minutes\n" "$BACKUP_INTERVAL_MINUTES"
|
||||
printf " Daily backup time: %s:00\n" "$BACKUP_DAILY_TIME"
|
||||
echo
|
||||
fi
|
||||
|
||||
printf "${GREEN}${ICON_SUCCESS} Backup status check complete!${NC}\n"
|
||||
echo
|
||||
}
|
||||
|
||||
main "$@"
|
||||
178
scripts/bash/db-guard.sh
Normal file
178
scripts/bash/db-guard.sh
Normal file
@@ -0,0 +1,178 @@
|
||||
#!/bin/bash
|
||||
# Continuously ensure the MySQL runtime tmpfs contains the restored data.
|
||||
# If the runtime tables are missing (for example after a host reboot),
|
||||
# automatically rerun db-import-conditional to hydrate from backups.
|
||||
set -euo pipefail
|
||||
|
||||
log(){ echo "🛡️ [db-guard] $*"; }
|
||||
warn(){ echo "⚠️ [db-guard] $*" >&2; }
|
||||
err(){ echo "❌ [db-guard] $*" >&2; }
|
||||
|
||||
MYSQL_HOST="${CONTAINER_MYSQL:-ac-mysql}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
MYSQL_USER="${MYSQL_USER:-root}"
|
||||
MYSQL_PASS="${MYSQL_ROOT_PASSWORD:-root}"
|
||||
IMPORT_SCRIPT="${DB_GUARD_IMPORT_SCRIPT:-/tmp/db-import-conditional.sh}"
|
||||
|
||||
RECHECK_SECONDS="${DB_GUARD_RECHECK_SECONDS:-120}"
|
||||
RETRY_SECONDS="${DB_GUARD_RETRY_SECONDS:-10}"
|
||||
WAIT_ATTEMPTS="${DB_GUARD_WAIT_ATTEMPTS:-60}"
|
||||
VERIFY_INTERVAL="${DB_GUARD_VERIFY_INTERVAL_SECONDS:-0}"
|
||||
VERIFY_FILE="${DB_GUARD_VERIFY_FILE:-/tmp/db-guard.last-verify}"
|
||||
HEALTH_FILE="${DB_GUARD_HEALTH_FILE:-/tmp/db-guard.ready}"
|
||||
STATUS_FILE="${DB_GUARD_STATUS_FILE:-/tmp/db-guard.status}"
|
||||
ERROR_FILE="${DB_GUARD_ERROR_FILE:-/tmp/db-guard.error}"
|
||||
MODULE_SQL_HOST_PATH="${MODULE_SQL_HOST_PATH:-/modules-sql}"
|
||||
|
||||
declare -a DB_SCHEMAS=()
|
||||
for var in DB_AUTH_NAME DB_WORLD_NAME DB_CHARACTERS_NAME DB_PLAYERBOTS_NAME; do
|
||||
value="${!var:-}"
|
||||
if [ -n "$value" ]; then
|
||||
DB_SCHEMAS+=("$value")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "${DB_GUARD_EXTRA_DATABASES:-}" ]; then
|
||||
IFS=',' read -ra extra <<< "${DB_GUARD_EXTRA_DATABASES}"
|
||||
for db in "${extra[@]}"; do
|
||||
if [ -n "${db// }" ]; then
|
||||
DB_SCHEMAS+=("${db// }")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "${#DB_SCHEMAS[@]}" -eq 0 ]; then
|
||||
DB_SCHEMAS=(acore_auth acore_world acore_characters)
|
||||
fi
|
||||
|
||||
SCHEMA_LIST_SQL="$(printf "'%s'," "${DB_SCHEMAS[@]}")"
|
||||
SCHEMA_LIST_SQL="${SCHEMA_LIST_SQL%,}"
|
||||
|
||||
mark_ready(){
|
||||
mkdir -p "$(dirname "$HEALTH_FILE")" 2>/dev/null || true
|
||||
printf '%s\t%s\n' "$(date -Iseconds)" "$*" | tee "$STATUS_FILE" >/dev/null
|
||||
: > "$ERROR_FILE"
|
||||
printf '%s\n' "$*" > "$HEALTH_FILE"
|
||||
}
|
||||
|
||||
mark_unhealthy(){
|
||||
printf '%s\t%s\n' "$(date -Iseconds)" "$*" | tee "$ERROR_FILE" >&2
|
||||
rm -f "$HEALTH_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
wait_for_mysql(){
|
||||
local attempts="$WAIT_ATTEMPTS"
|
||||
while [ "$attempts" -gt 0 ]; do
|
||||
if MYSQL_PWD="$MYSQL_PASS" mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -e "SELECT 1" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
attempts=$((attempts - 1))
|
||||
sleep "$RETRY_SECONDS"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
table_count(){
|
||||
local query="SELECT COUNT(*) FROM information_schema.tables WHERE table_schema IN (${SCHEMA_LIST_SQL});"
|
||||
MYSQL_PWD="$MYSQL_PASS" mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -N -B -e "$query"
|
||||
}
|
||||
|
||||
rehydrate(){
|
||||
if [ ! -x "$IMPORT_SCRIPT" ]; then
|
||||
err "Import script not found at ${IMPORT_SCRIPT}"
|
||||
return 1
|
||||
fi
|
||||
"$IMPORT_SCRIPT"
|
||||
}
|
||||
|
||||
ensure_dbimport_conf(){
|
||||
local conf="/azerothcore/env/dist/etc/dbimport.conf"
|
||||
local dist="${conf}.dist"
|
||||
if [ ! -f "$conf" ] && [ -f "$dist" ]; then
|
||||
cp "$dist" "$conf"
|
||||
fi
|
||||
mkdir -p /azerothcore/env/dist/temp
|
||||
}
|
||||
|
||||
sync_host_stage_files(){
|
||||
local host_root="${MODULE_SQL_HOST_PATH}"
|
||||
[ -d "$host_root" ] || return 0
|
||||
for dir in db_world db_characters db_auth db_playerbots; do
|
||||
local src="$host_root/$dir"
|
||||
local dest="/azerothcore/data/sql/updates/$dir"
|
||||
mkdir -p "$dest"
|
||||
rm -f "$dest"/MODULE_*.sql >/dev/null 2>&1 || true
|
||||
if [ -d "$src" ]; then
|
||||
cp -a "$src"/MODULE_*.sql "$dest"/ >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
dbimport_verify(){
|
||||
local bin_dir="/azerothcore/env/dist/bin"
|
||||
ensure_dbimport_conf
|
||||
sync_host_stage_files
|
||||
if [ ! -x "${bin_dir}/dbimport" ]; then
|
||||
warn "dbimport binary not found at ${bin_dir}/dbimport"
|
||||
return 1
|
||||
fi
|
||||
log "Running dbimport verification sweep..."
|
||||
if (cd "$bin_dir" && ./dbimport); then
|
||||
log "dbimport verification finished successfully"
|
||||
return 0
|
||||
fi
|
||||
warn "dbimport verification reported issues - review dbimport logs"
|
||||
return 1
|
||||
}
|
||||
|
||||
maybe_run_verification(){
|
||||
if [ "${VERIFY_INTERVAL}" -lt 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
local now last_run=0
|
||||
now="$(date +%s)"
|
||||
if [ -f "$VERIFY_FILE" ]; then
|
||||
last_run="$(cat "$VERIFY_FILE" 2>/dev/null || echo 0)"
|
||||
if [ "$VERIFY_INTERVAL" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ $((now - last_run)) -lt "${VERIFY_INTERVAL}" ]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if dbimport_verify; then
|
||||
echo "$now" > "$VERIFY_FILE"
|
||||
else
|
||||
warn "dbimport verification failed; will retry in ${VERIFY_INTERVAL}s"
|
||||
fi
|
||||
}
|
||||
|
||||
log "Watching MySQL (${MYSQL_HOST}:${MYSQL_PORT}) for ${#DB_SCHEMAS[@]} schemas: ${DB_SCHEMAS[*]}"
|
||||
|
||||
while true; do
|
||||
if ! wait_for_mysql; then
|
||||
mark_unhealthy "MySQL is unreachable after ${WAIT_ATTEMPTS} attempts"
|
||||
sleep "$RETRY_SECONDS"
|
||||
continue
|
||||
fi
|
||||
|
||||
count="$(table_count 2>/dev/null || echo "")"
|
||||
if [ -n "$count" ]; then
|
||||
if [ "$count" -gt 0 ] 2>/dev/null; then
|
||||
mark_ready "Detected ${count} tables across tracked schemas"
|
||||
maybe_run_verification
|
||||
sleep "$RECHECK_SECONDS"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
warn "No tables detected across ${DB_SCHEMAS[*]}; running rehydrate workflow..."
|
||||
if rehydrate; then
|
||||
log "Rehydrate complete - rechecking tables"
|
||||
sleep 5
|
||||
continue
|
||||
fi
|
||||
|
||||
mark_unhealthy "Rehydrate workflow failed - retrying in ${RETRY_SECONDS}s"
|
||||
sleep "$RETRY_SECONDS"
|
||||
done
|
||||
389
scripts/bash/db-health-check.sh
Executable file
389
scripts/bash/db-health-check.sh
Executable file
@@ -0,0 +1,389 @@
|
||||
#!/bin/bash
|
||||
# Database Health Check Script
|
||||
# Provides comprehensive health status of AzerothCore databases
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
# Colors
|
||||
BLUE='\033[0;34m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Icons
|
||||
ICON_SUCCESS="✅"
|
||||
ICON_WARNING="⚠️"
|
||||
ICON_ERROR="❌"
|
||||
ICON_INFO="ℹ️"
|
||||
ICON_DB="🗄️"
|
||||
ICON_SIZE="💾"
|
||||
ICON_TIME="🕐"
|
||||
ICON_MODULE="📦"
|
||||
ICON_UPDATE="🔄"
|
||||
|
||||
# Default values
|
||||
VERBOSE=0
|
||||
SHOW_PENDING=0
|
||||
SHOW_MODULES=1
|
||||
CONTAINER_NAME="ac-mysql"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./db-health-check.sh [options]
|
||||
|
||||
Check the health status of AzerothCore databases.
|
||||
|
||||
Options:
|
||||
-v, --verbose Show detailed information
|
||||
-p, --pending Show pending updates
|
||||
-m, --no-modules Hide module update information
|
||||
-c, --container NAME MySQL container name (default: ac-mysql)
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
./db-health-check.sh
|
||||
./db-health-check.sh --verbose --pending
|
||||
./db-health-check.sh --container ac-mysql-custom
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-v|--verbose) VERBOSE=1; shift;;
|
||||
-p|--pending) SHOW_PENDING=1; shift;;
|
||||
-m|--no-modules) SHOW_MODULES=0; shift;;
|
||||
-c|--container) CONTAINER_NAME="$2"; shift 2;;
|
||||
-h|--help) usage; exit 0;;
|
||||
*) echo "Unknown option: $1"; usage; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Load environment
|
||||
if [ -f "$PROJECT_ROOT/.env" ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$PROJECT_ROOT/.env"
|
||||
set +a
|
||||
fi
|
||||
|
||||
MYSQL_HOST="${MYSQL_HOST:-ac-mysql}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
MYSQL_USER="${MYSQL_USER:-root}"
|
||||
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}"
|
||||
DB_AUTH_NAME="${DB_AUTH_NAME:-acore_auth}"
|
||||
DB_WORLD_NAME="${DB_WORLD_NAME:-acore_world}"
|
||||
DB_CHARACTERS_NAME="${DB_CHARACTERS_NAME:-acore_characters}"
|
||||
DB_PLAYERBOTS_NAME="${DB_PLAYERBOTS_NAME:-acore_playerbots}"
|
||||
|
||||
# MySQL query helper
|
||||
mysql_query() {
|
||||
local database="${1:-}"
|
||||
local query="$2"
|
||||
|
||||
if [ -z "$MYSQL_ROOT_PASSWORD" ]; then
|
||||
echo "Error: MYSQL_ROOT_PASSWORD not set" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
if [ -n "$database" ]; then
|
||||
docker exec "$CONTAINER_NAME" mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_ROOT_PASSWORD" "$database" -N -B -e "$query" 2>/dev/null
|
||||
else
|
||||
docker exec "$CONTAINER_NAME" mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_ROOT_PASSWORD" -N -B -e "$query" 2>/dev/null
|
||||
fi
|
||||
else
|
||||
if [ -n "$database" ]; then
|
||||
mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_ROOT_PASSWORD" "$database" -N -B -e "$query" 2>/dev/null
|
||||
else
|
||||
mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_ROOT_PASSWORD" -N -B -e "$query" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Format bytes to human readable
|
||||
format_bytes() {
|
||||
local bytes=$1
|
||||
if [ "$bytes" -lt 1024 ]; then
|
||||
echo "${bytes}B"
|
||||
elif [ "$bytes" -lt 1048576 ]; then
|
||||
echo "$(awk "BEGIN {printf \"%.1f\", $bytes/1024}")KB"
|
||||
elif [ "$bytes" -lt 1073741824 ]; then
|
||||
echo "$(awk "BEGIN {printf \"%.1f\", $bytes/1048576}")MB"
|
||||
else
|
||||
echo "$(awk "BEGIN {printf \"%.2f\", $bytes/1073741824}")GB"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if database exists
|
||||
db_exists() {
|
||||
local db_name="$1"
|
||||
local count
|
||||
count=$(mysql_query "" "SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$db_name'" 2>/dev/null || echo "0")
|
||||
[ "$count" = "1" ]
|
||||
}
|
||||
|
||||
# Get database size
|
||||
get_db_size() {
|
||||
local db_name="$1"
|
||||
mysql_query "" "SELECT IFNULL(SUM(data_length + index_length), 0) FROM information_schema.TABLES WHERE table_schema='$db_name'" 2>/dev/null || echo "0"
|
||||
}
|
||||
|
||||
# Get update count
|
||||
get_update_count() {
|
||||
local db_name="$1"
|
||||
local state="${2:-}"
|
||||
|
||||
if [ -n "$state" ]; then
|
||||
mysql_query "$db_name" "SELECT COUNT(*) FROM updates WHERE state='$state'" 2>/dev/null || echo "0"
|
||||
else
|
||||
mysql_query "$db_name" "SELECT COUNT(*) FROM updates" 2>/dev/null || echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get last update timestamp
|
||||
get_last_update() {
|
||||
local db_name="$1"
|
||||
mysql_query "$db_name" "SELECT IFNULL(MAX(timestamp), 'Never') FROM updates" 2>/dev/null || echo "Never"
|
||||
}
|
||||
|
||||
# Get table count
|
||||
get_table_count() {
|
||||
local db_name="$1"
|
||||
mysql_query "" "SELECT COUNT(*) FROM information_schema.TABLES WHERE table_schema='$db_name'" 2>/dev/null || echo "0"
|
||||
}
|
||||
|
||||
# Get character count
|
||||
get_character_count() {
|
||||
mysql_query "$DB_CHARACTERS_NAME" "SELECT COUNT(*) FROM characters" 2>/dev/null || echo "0"
|
||||
}
|
||||
|
||||
# Get active players (logged in last 24 hours)
|
||||
get_active_players() {
|
||||
mysql_query "$DB_CHARACTERS_NAME" "SELECT COUNT(*) FROM characters WHERE logout_time > UNIX_TIMESTAMP(NOW() - INTERVAL 1 DAY)" 2>/dev/null || echo "0"
|
||||
}
|
||||
|
||||
# Get account count
|
||||
get_account_count() {
|
||||
mysql_query "$DB_AUTH_NAME" "SELECT COUNT(*) FROM account" 2>/dev/null || echo "0"
|
||||
}
|
||||
|
||||
# Get pending updates
|
||||
get_pending_updates() {
|
||||
local db_name="$1"
|
||||
mysql_query "$db_name" "SELECT name FROM updates WHERE state='PENDING' ORDER BY name" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Check database health
|
||||
check_database() {
|
||||
local db_name="$1"
|
||||
local display_name="$2"
|
||||
|
||||
if ! db_exists "$db_name"; then
|
||||
printf " ${RED}${ICON_ERROR} %s (%s)${NC}\n" "$display_name" "$db_name"
|
||||
printf " ${RED}Database does not exist${NC}\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf " ${GREEN}${ICON_SUCCESS} %s (%s)${NC}\n" "$display_name" "$db_name"
|
||||
|
||||
local update_count module_count last_update db_size table_count
|
||||
update_count=$(get_update_count "$db_name" "RELEASED")
|
||||
module_count=$(get_update_count "$db_name" "MODULE")
|
||||
last_update=$(get_last_update "$db_name")
|
||||
db_size=$(get_db_size "$db_name")
|
||||
table_count=$(get_table_count "$db_name")
|
||||
|
||||
printf " ${ICON_UPDATE} Updates: %s applied" "$update_count"
|
||||
if [ "$module_count" != "0" ] && [ "$SHOW_MODULES" = "1" ]; then
|
||||
printf " (%s module)" "$module_count"
|
||||
fi
|
||||
printf "\n"
|
||||
|
||||
printf " ${ICON_TIME} Last update: %s\n" "$last_update"
|
||||
printf " ${ICON_SIZE} Size: %s (%s tables)\n" "$(format_bytes "$db_size")" "$table_count"
|
||||
|
||||
if [ "$VERBOSE" = "1" ]; then
|
||||
local custom_count archived_count
|
||||
custom_count=$(get_update_count "$db_name" "CUSTOM")
|
||||
archived_count=$(get_update_count "$db_name" "ARCHIVED")
|
||||
|
||||
if [ "$custom_count" != "0" ]; then
|
||||
printf " ${ICON_INFO} Custom updates: %s\n" "$custom_count"
|
||||
fi
|
||||
if [ "$archived_count" != "0" ]; then
|
||||
printf " ${ICON_INFO} Archived updates: %s\n" "$archived_count"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show pending updates if requested
|
||||
if [ "$SHOW_PENDING" = "1" ]; then
|
||||
local pending_updates
|
||||
pending_updates=$(get_pending_updates "$db_name")
|
||||
if [ -n "$pending_updates" ]; then
|
||||
printf " ${YELLOW}${ICON_WARNING} Pending updates:${NC}\n"
|
||||
while IFS= read -r update; do
|
||||
printf " - %s\n" "$update"
|
||||
done <<< "$pending_updates"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
}
|
||||
|
||||
# Show module updates summary
|
||||
show_module_updates() {
|
||||
if [ "$SHOW_MODULES" = "0" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
printf "${BOLD}${ICON_MODULE} Module Updates${NC}\n"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Get module updates from world database (most modules update world DB)
|
||||
local module_updates
|
||||
module_updates=$(mysql_query "$DB_WORLD_NAME" "SELECT SUBSTRING_INDEX(name, '_', 1) as module, COUNT(*) as count FROM updates WHERE state='MODULE' GROUP BY module ORDER BY module" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$module_updates" ]; then
|
||||
printf " ${ICON_INFO} No module updates detected\n\n"
|
||||
return
|
||||
fi
|
||||
|
||||
while IFS=$'\t' read -r module count; do
|
||||
printf " ${GREEN}${ICON_SUCCESS}${NC} %s: %s update(s)\n" "$module" "$count"
|
||||
done <<< "$module_updates"
|
||||
echo
|
||||
}
|
||||
|
||||
# Get backup information
|
||||
get_backup_info() {
|
||||
local backup_dir="$PROJECT_ROOT/storage/backups"
|
||||
|
||||
if [ ! -d "$backup_dir" ]; then
|
||||
printf " ${ICON_INFO} No backups directory found\n"
|
||||
return
|
||||
fi
|
||||
|
||||
# Check for latest backup
|
||||
local latest_hourly latest_daily
|
||||
if [ -d "$backup_dir/hourly" ]; then
|
||||
latest_hourly=$(ls -1t "$backup_dir/hourly" 2>/dev/null | head -n1 || echo "")
|
||||
fi
|
||||
if [ -d "$backup_dir/daily" ]; then
|
||||
latest_daily=$(ls -1t "$backup_dir/daily" 2>/dev/null | head -n1 || echo "")
|
||||
fi
|
||||
|
||||
if [ -n "$latest_hourly" ]; then
|
||||
# Calculate time ago
|
||||
local backup_timestamp="${latest_hourly:0:8}_${latest_hourly:9:6}"
|
||||
local backup_epoch
|
||||
backup_epoch=$(date -d "${backup_timestamp:0:4}-${backup_timestamp:4:2}-${backup_timestamp:6:2} ${backup_timestamp:9:2}:${backup_timestamp:11:2}:${backup_timestamp:13:2}" +%s 2>/dev/null || echo "0")
|
||||
local now_epoch
|
||||
now_epoch=$(date +%s)
|
||||
local diff=$((now_epoch - backup_epoch))
|
||||
local hours=$((diff / 3600))
|
||||
local minutes=$(((diff % 3600) / 60))
|
||||
|
||||
if [ "$hours" -gt 0 ]; then
|
||||
printf " ${ICON_TIME} Last hourly backup: %s hours ago\n" "$hours"
|
||||
else
|
||||
printf " ${ICON_TIME} Last hourly backup: %s minutes ago\n" "$minutes"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$latest_daily" ] && [ "$latest_daily" != "$latest_hourly" ]; then
|
||||
local backup_timestamp="${latest_daily:0:8}_${latest_daily:9:6}"
|
||||
local backup_epoch
|
||||
backup_epoch=$(date -d "${backup_timestamp:0:4}-${backup_timestamp:4:2}-${backup_timestamp:6:2} ${backup_timestamp:9:2}:${backup_timestamp:11:2}:${backup_timestamp:13:2}" +%s 2>/dev/null || echo "0")
|
||||
local now_epoch
|
||||
now_epoch=$(date +%s)
|
||||
local diff=$((now_epoch - backup_epoch))
|
||||
local days=$((diff / 86400))
|
||||
|
||||
printf " ${ICON_TIME} Last daily backup: %s days ago\n" "$days"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main health check
|
||||
main() {
|
||||
echo
|
||||
printf "${BOLD}${BLUE}${ICON_DB} AZEROTHCORE DATABASE HEALTH CHECK${NC}\n"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo
|
||||
|
||||
# Test MySQL connection
|
||||
if ! mysql_query "" "SELECT 1" >/dev/null 2>&1; then
|
||||
printf "${RED}${ICON_ERROR} Cannot connect to MySQL server${NC}\n"
|
||||
printf " Host: %s:%s\n" "$MYSQL_HOST" "$MYSQL_PORT"
|
||||
printf " User: %s\n" "$MYSQL_USER"
|
||||
printf " Container: %s\n\n" "$CONTAINER_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "${BOLD}${ICON_DB} Database Status${NC}\n"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo
|
||||
|
||||
# Check each database
|
||||
check_database "$DB_AUTH_NAME" "Auth DB"
|
||||
check_database "$DB_WORLD_NAME" "World DB"
|
||||
check_database "$DB_CHARACTERS_NAME" "Characters DB"
|
||||
|
||||
# Optional: Check playerbots database
|
||||
if db_exists "$DB_PLAYERBOTS_NAME"; then
|
||||
check_database "$DB_PLAYERBOTS_NAME" "Playerbots DB"
|
||||
fi
|
||||
|
||||
# Show character/account statistics
|
||||
printf "${BOLD}${CYAN}📊 Server Statistics${NC}\n"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
local account_count character_count active_count
|
||||
account_count=$(get_account_count)
|
||||
character_count=$(get_character_count)
|
||||
active_count=$(get_active_players)
|
||||
|
||||
printf " ${ICON_INFO} Accounts: %s\n" "$account_count"
|
||||
printf " ${ICON_INFO} Characters: %s\n" "$character_count"
|
||||
printf " ${ICON_INFO} Active (24h): %s\n" "$active_count"
|
||||
echo
|
||||
|
||||
# Show module updates
|
||||
show_module_updates
|
||||
|
||||
# Show backup information
|
||||
printf "${BOLD}${ICON_SIZE} Backup Information${NC}\n"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
get_backup_info
|
||||
echo
|
||||
|
||||
# Calculate total database size
|
||||
local total_size=0
|
||||
for db in "$DB_AUTH_NAME" "$DB_WORLD_NAME" "$DB_CHARACTERS_NAME"; do
|
||||
if db_exists "$db"; then
|
||||
local size
|
||||
size=$(get_db_size "$db")
|
||||
total_size=$((total_size + size))
|
||||
fi
|
||||
done
|
||||
|
||||
if db_exists "$DB_PLAYERBOTS_NAME"; then
|
||||
local size
|
||||
size=$(get_db_size "$DB_PLAYERBOTS_NAME")
|
||||
total_size=$((total_size + size))
|
||||
fi
|
||||
|
||||
printf "${BOLD}💾 Total Database Storage: %s${NC}\n" "$(format_bytes "$total_size")"
|
||||
echo
|
||||
|
||||
printf "${GREEN}${ICON_SUCCESS} Health check complete!${NC}\n"
|
||||
echo
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -34,6 +34,62 @@ Notes:
|
||||
EOF
|
||||
}
|
||||
|
||||
verify_databases_populated() {
|
||||
local mysql_host="${CONTAINER_MYSQL:-ac-mysql}"
|
||||
local mysql_port="${MYSQL_PORT:-3306}"
|
||||
local mysql_user="${MYSQL_USER:-root}"
|
||||
local mysql_pass="${MYSQL_ROOT_PASSWORD:-root}"
|
||||
local db_auth="${DB_AUTH_NAME:-acore_auth}"
|
||||
local db_world="${DB_WORLD_NAME:-acore_world}"
|
||||
local db_characters="${DB_CHARACTERS_NAME:-acore_characters}"
|
||||
|
||||
if ! command -v mysql >/dev/null 2>&1; then
|
||||
echo "⚠️ mysql client is not available to verify restoration status"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local query="SELECT COUNT(*) FROM information_schema.tables WHERE table_schema IN ('$db_auth','$db_world','$db_characters');"
|
||||
local table_count
|
||||
if ! table_count=$(MYSQL_PWD="$mysql_pass" mysql -h "$mysql_host" -P "$mysql_port" -u "$mysql_user" -N -B -e "$query" 2>/dev/null); then
|
||||
echo "⚠️ Unable to query MySQL at ${mysql_host}:${mysql_port} to verify restoration status"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "${table_count:-0}" -gt 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "⚠️ MySQL is reachable but no AzerothCore tables were found"
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_mysql(){
|
||||
local mysql_host="${CONTAINER_MYSQL:-ac-mysql}"
|
||||
local mysql_port="${MYSQL_PORT:-3306}"
|
||||
local mysql_user="${MYSQL_USER:-root}"
|
||||
local mysql_pass="${MYSQL_ROOT_PASSWORD:-root}"
|
||||
local max_attempts=30
|
||||
local delay=2
|
||||
while [ $max_attempts -gt 0 ]; do
|
||||
if MYSQL_PWD="$mysql_pass" mysql -h "$mysql_host" -P "$mysql_port" -u "$mysql_user" -e "SELECT 1" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
max_attempts=$((max_attempts - 1))
|
||||
sleep "$delay"
|
||||
done
|
||||
echo "❌ Unable to connect to MySQL at ${mysql_host}:${mysql_port} after multiple attempts"
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_dbimport_conf(){
|
||||
local conf="/azerothcore/env/dist/etc/dbimport.conf"
|
||||
local dist="${conf}.dist"
|
||||
if [ ! -f "$conf" ] && [ -f "$dist" ]; then
|
||||
cp "$dist" "$conf"
|
||||
fi
|
||||
mkdir -p /azerothcore/env/dist/temp
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help)
|
||||
print_help
|
||||
@@ -50,6 +106,11 @@ esac
|
||||
echo "🔧 Conditional AzerothCore Database Import"
|
||||
echo "========================================"
|
||||
|
||||
if ! wait_for_mysql; then
|
||||
echo "❌ MySQL service is unavailable; aborting database import"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Restoration status markers - use writable location
|
||||
RESTORE_STATUS_DIR="/var/lib/mysql-persistent"
|
||||
MARKER_STATUS_DIR="/tmp"
|
||||
@@ -70,10 +131,17 @@ fi
|
||||
echo "🔍 Checking restoration status..."
|
||||
|
||||
if [ -f "$RESTORE_SUCCESS_MARKER" ]; then
|
||||
echo "✅ Backup restoration completed successfully"
|
||||
cat "$RESTORE_SUCCESS_MARKER" || true
|
||||
echo "🚫 Skipping database import - data already restored from backup"
|
||||
exit 0
|
||||
if verify_databases_populated; then
|
||||
echo "✅ Backup restoration completed successfully"
|
||||
cat "$RESTORE_SUCCESS_MARKER" || true
|
||||
echo "🚫 Skipping database import - data already restored from backup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "⚠️ Restoration marker found, but databases are empty - forcing re-import"
|
||||
rm -f "$RESTORE_SUCCESS_MARKER" 2>/dev/null || true
|
||||
rm -f "$RESTORE_SUCCESS_MARKER_TMP" 2>/dev/null || true
|
||||
rm -f "$RESTORE_FAILED_MARKER" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -f "$RESTORE_FAILED_MARKER" ]; then
|
||||
@@ -280,9 +348,70 @@ if [ -n "$backup_path" ]; then
|
||||
return $([ "$restore_success" = true ] && echo 0 || echo 1)
|
||||
}
|
||||
|
||||
verify_and_update_restored_databases() {
|
||||
echo "🔍 Verifying restored database integrity..."
|
||||
|
||||
# Check if dbimport is available
|
||||
if [ ! -f "/azerothcore/env/dist/bin/dbimport" ]; then
|
||||
echo "⚠️ dbimport not available, skipping verification"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ensure_dbimport_conf
|
||||
|
||||
cd /azerothcore/env/dist/bin
|
||||
echo "🔄 Running dbimport to apply any missing updates..."
|
||||
if ./dbimport; then
|
||||
echo "✅ Database verification complete - all updates current"
|
||||
else
|
||||
echo "⚠️ dbimport reported issues - check logs"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify critical tables exist
|
||||
echo "🔍 Checking critical tables..."
|
||||
local critical_tables=("account" "characters" "creature" "quest_template")
|
||||
local missing_tables=0
|
||||
|
||||
for table in "${critical_tables[@]}"; do
|
||||
local db_name="$DB_WORLD_NAME"
|
||||
case "$table" in
|
||||
account) db_name="$DB_AUTH_NAME" ;;
|
||||
characters) db_name="$DB_CHARACTERS_NAME" ;;
|
||||
esac
|
||||
|
||||
if ! mysql -h ${CONTAINER_MYSQL} -u${MYSQL_USER} -p${MYSQL_ROOT_PASSWORD} \
|
||||
-e "SELECT 1 FROM ${db_name}.${table} LIMIT 1" >/dev/null 2>&1; then
|
||||
echo "⚠️ Critical table missing: ${db_name}.${table}"
|
||||
missing_tables=$((missing_tables + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$missing_tables" -gt 0 ]; then
|
||||
echo "⚠️ ${missing_tables} critical tables missing after restore"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "✅ All critical tables verified"
|
||||
return 0
|
||||
}
|
||||
|
||||
if restore_backup "$backup_path"; then
|
||||
echo "$(date): Backup successfully restored from $backup_path" > "$RESTORE_SUCCESS_MARKER"
|
||||
echo "🎉 Backup restoration completed successfully!"
|
||||
|
||||
# Verify and apply missing updates
|
||||
verify_and_update_restored_databases
|
||||
|
||||
if [ -x "/tmp/restore-and-stage.sh" ]; then
|
||||
echo "🔧 Running restore-time module SQL staging..."
|
||||
MODULES_DIR="/modules" \
|
||||
RESTORE_SOURCE_DIR="$backup_path" \
|
||||
/tmp/restore-and-stage.sh
|
||||
else
|
||||
echo "ℹ️ restore-and-stage helper not available; skipping automatic module SQL staging"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
else
|
||||
echo "$(date): Backup restoration failed - proceeding with fresh setup" > "$RESTORE_FAILED_MARKER"
|
||||
@@ -302,29 +431,7 @@ CREATE DATABASE IF NOT EXISTS acore_playerbots DEFAULT CHARACTER SET utf8mb4 COL
|
||||
SHOW DATABASES;" || { echo "❌ Failed to create databases"; exit 1; }
|
||||
echo "✅ Fresh databases created - proceeding with schema import"
|
||||
|
||||
echo "📝 Creating dbimport configuration..."
|
||||
mkdir -p /azerothcore/env/dist/etc
|
||||
TEMP_DIR="/azerothcore/env/dist/temp"
|
||||
mkdir -p "$TEMP_DIR"
|
||||
MYSQL_EXECUTABLE="$(command -v mysql || echo '/usr/bin/mysql')"
|
||||
cat > /azerothcore/env/dist/etc/dbimport.conf <<EOF
|
||||
LoginDatabaseInfo = "${CONTAINER_MYSQL};${MYSQL_PORT};${MYSQL_USER};${MYSQL_ROOT_PASSWORD};${DB_AUTH_NAME}"
|
||||
WorldDatabaseInfo = "${CONTAINER_MYSQL};${MYSQL_PORT};${MYSQL_USER};${MYSQL_ROOT_PASSWORD};${DB_WORLD_NAME}"
|
||||
CharacterDatabaseInfo = "${CONTAINER_MYSQL};${MYSQL_PORT};${MYSQL_USER};${MYSQL_ROOT_PASSWORD};${DB_CHARACTERS_NAME}"
|
||||
Updates.EnableDatabases = 7
|
||||
Updates.AutoSetup = 1
|
||||
TempDir = "${TEMP_DIR}"
|
||||
MySQLExecutable = "${MYSQL_EXECUTABLE}"
|
||||
Updates.AllowedModules = "all"
|
||||
LoginDatabase.WorkerThreads = 1
|
||||
LoginDatabase.SynchThreads = 1
|
||||
WorldDatabase.WorkerThreads = 1
|
||||
WorldDatabase.SynchThreads = 1
|
||||
CharacterDatabase.WorkerThreads = 1
|
||||
CharacterDatabase.SynchThreads = 1
|
||||
SourceDirectory = "/azerothcore"
|
||||
Updates.ExceptionShutdownDelay = 10000
|
||||
EOF
|
||||
ensure_dbimport_conf
|
||||
|
||||
echo "🚀 Running database import..."
|
||||
cd /azerothcore/env/dist/bin
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Copy user database files or full backup archives from database-import/ to backup system
|
||||
# Copy user database files or full backup archives from import/db/ or database-import/ to backup system
|
||||
set -euo pipefail
|
||||
|
||||
# Source environment variables
|
||||
@@ -9,10 +9,20 @@ if [ -f ".env" ]; then
|
||||
set +a
|
||||
fi
|
||||
|
||||
IMPORT_DIR="./database-import"
|
||||
# Support both new (import/db) and legacy (database-import) directories
|
||||
IMPORT_DIR_NEW="./import/db"
|
||||
IMPORT_DIR_LEGACY="./database-import"
|
||||
|
||||
# Prefer new directory if it has files, otherwise fall back to legacy
|
||||
IMPORT_DIR="$IMPORT_DIR_NEW"
|
||||
if [ ! -d "$IMPORT_DIR" ] || [ -z "$(ls -A "$IMPORT_DIR" 2>/dev/null)" ]; then
|
||||
IMPORT_DIR="$IMPORT_DIR_LEGACY"
|
||||
fi
|
||||
STORAGE_PATH="${STORAGE_PATH:-./storage}"
|
||||
STORAGE_PATH_LOCAL="${STORAGE_PATH_LOCAL:-./local-storage}"
|
||||
BACKUP_ROOT="${STORAGE_PATH}/backups"
|
||||
MYSQL_DATA_VOLUME_NAME="${MYSQL_DATA_VOLUME_NAME:-mysql-data}"
|
||||
ALPINE_IMAGE="${ALPINE_IMAGE:-alpine:latest}"
|
||||
|
||||
shopt -s nullglob
|
||||
sql_files=("$IMPORT_DIR"/*.sql "$IMPORT_DIR"/*.sql.gz)
|
||||
@@ -24,7 +34,25 @@ if [ ! -d "$IMPORT_DIR" ] || [ ${#sql_files[@]} -eq 0 ]; then
|
||||
fi
|
||||
|
||||
# Exit if backup system already has databases restored
|
||||
if [ -f "${STORAGE_PATH_LOCAL}/mysql-data/.restore-completed" ]; then
|
||||
has_restore_marker(){
|
||||
# Prefer Docker volume marker (post-migration), fall back to legacy host path
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
if docker volume inspect "$MYSQL_DATA_VOLUME_NAME" >/dev/null 2>&1; then
|
||||
if docker run --rm \
|
||||
-v "${MYSQL_DATA_VOLUME_NAME}:/var/lib/mysql-persistent" \
|
||||
"$ALPINE_IMAGE" \
|
||||
sh -c 'test -f /var/lib/mysql-persistent/.restore-completed' >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ -f "${STORAGE_PATH_LOCAL}/mysql-data/.restore-completed" ]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if has_restore_marker; then
|
||||
echo "✅ Database already restored - skipping import"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -477,20 +477,11 @@ load_sql_helper(){
|
||||
err "SQL helper not found; expected manage-modules-sql.sh to be available"
|
||||
}
|
||||
|
||||
execute_module_sql(){
|
||||
SQL_EXECUTION_FAILED=0
|
||||
if declare -f execute_module_sql_scripts >/dev/null 2>&1; then
|
||||
echo 'Executing module SQL scripts...'
|
||||
if execute_module_sql_scripts; then
|
||||
echo 'SQL execution complete.'
|
||||
else
|
||||
echo '⚠️ Module SQL scripts reported errors'
|
||||
SQL_EXECUTION_FAILED=1
|
||||
fi
|
||||
else
|
||||
info "SQL helper did not expose execute_module_sql_scripts; skipping module SQL execution"
|
||||
fi
|
||||
}
|
||||
# REMOVED: stage_module_sql_files() and execute_module_sql()
|
||||
# These functions were part of build-time SQL staging that created files in
|
||||
# /azerothcore/modules/*/data/sql/updates/ which are NEVER scanned by AzerothCore's DBUpdater.
|
||||
# Module SQL is now staged at runtime by stage-modules.sh which copies files to
|
||||
# /azerothcore/data/sql/updates/ (core directory) where they ARE scanned and processed.
|
||||
|
||||
track_module_state(){
|
||||
echo 'Checking for module changes that require rebuild...'
|
||||
@@ -591,20 +582,11 @@ main(){
|
||||
remove_disabled_modules
|
||||
install_enabled_modules
|
||||
manage_configuration_files
|
||||
info "SQL execution gate: MODULES_SKIP_SQL=${MODULES_SKIP_SQL:-0}"
|
||||
if [ "${MODULES_SKIP_SQL:-0}" = "1" ]; then
|
||||
info "Skipping module SQL execution (MODULES_SKIP_SQL=1)"
|
||||
else
|
||||
info "Initiating module SQL helper"
|
||||
load_sql_helper
|
||||
info "SQL helper loaded from ${SQL_HELPER_PATH:-unknown}"
|
||||
execute_module_sql
|
||||
fi
|
||||
track_module_state
|
||||
# NOTE: Module SQL staging is now handled at runtime by stage-modules.sh
|
||||
# which copies SQL files to /azerothcore/data/sql/updates/ after containers start.
|
||||
# Build-time SQL staging has been removed as it created files that were never processed.
|
||||
|
||||
if [ "${SQL_EXECUTION_FAILED:-0}" = "1" ]; then
|
||||
warn "Module SQL execution reported issues; review logs above."
|
||||
fi
|
||||
track_module_state
|
||||
|
||||
echo 'Module management complete.'
|
||||
|
||||
|
||||
139
scripts/bash/repair-storage-permissions.sh
Executable file
139
scripts/bash/repair-storage-permissions.sh
Executable file
@@ -0,0 +1,139 @@
|
||||
#!/bin/bash
|
||||
# Normalize permissions across storage/ and local-storage/ so host processes
|
||||
# (and CI tools) can read/write module metadata without manual chown.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
ENV_FILE="$PROJECT_ROOT/.env"
|
||||
TEMPLATE_FILE="$PROJECT_ROOT/.env.template"
|
||||
|
||||
usage(){
|
||||
cat <<'EOF'
|
||||
Usage: repair-storage-permissions.sh [options]
|
||||
|
||||
Ensures common storage directories are writable by the current host user.
|
||||
|
||||
Options:
|
||||
--path <dir> Additional directory to fix (can be passed multiple times)
|
||||
--silent Reduce output (only errors/warnings)
|
||||
-h, --help Show this help message
|
||||
EOF
|
||||
}
|
||||
|
||||
read_env(){
|
||||
local key="$1" default="$2" env_path="$ENV_FILE" value=""
|
||||
if [ -f "$env_path" ]; then
|
||||
value="$(grep -E "^${key}=" "$env_path" | tail -n1 | cut -d'=' -f2- | tr -d '\r')"
|
||||
fi
|
||||
if [ -z "$value" ] && [ -f "$TEMPLATE_FILE" ]; then
|
||||
value="$(grep -E "^${key}=" "$TEMPLATE_FILE" | tail -n1 | cut -d'=' -f2- | tr -d '\r')"
|
||||
fi
|
||||
if [ -z "$value" ]; then
|
||||
value="$default"
|
||||
fi
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
silent=0
|
||||
declare -a extra_paths=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--path)
|
||||
shift
|
||||
[ $# -gt 0 ] || { echo "Missing value for --path" >&2; exit 1; }
|
||||
extra_paths+=("$1")
|
||||
;;
|
||||
--silent)
|
||||
silent=1
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
log(){ [ "$silent" -eq 1 ] || echo "$*"; }
|
||||
warn(){ echo "⚠️ $*" >&2; }
|
||||
|
||||
resolve_path(){
|
||||
local path="$1"
|
||||
if [[ "$path" != /* ]]; then
|
||||
path="${path#./}"
|
||||
path="$PROJECT_ROOT/$path"
|
||||
fi
|
||||
printf '%s\n' "$(cd "$(dirname "$path")" 2>/dev/null && pwd 2>/dev/null)/$(basename "$path")"
|
||||
}
|
||||
|
||||
ensure_host_writable(){
|
||||
local target="$1"
|
||||
[ -n "$target" ] || return 0
|
||||
mkdir -p "$target" 2>/dev/null || true
|
||||
[ -d "$target" ] || { warn "Path not found: $target"; return 0; }
|
||||
|
||||
local uid gid
|
||||
uid="$(id -u)"
|
||||
gid="$(id -g)"
|
||||
|
||||
if chown -R "$uid":"$gid" "$target" 2>/dev/null; then
|
||||
:
|
||||
elif command -v docker >/dev/null 2>&1; then
|
||||
local helper_image
|
||||
helper_image="$(read_env ALPINE_IMAGE "alpine:latest")"
|
||||
if ! docker run --rm -u 0:0 -v "$target":/workspace "$helper_image" \
|
||||
sh -c "chown -R ${uid}:${gid} /workspace" >/dev/null 2>&1; then
|
||||
warn "Failed to adjust ownership for $target"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
warn "Cannot adjust ownership for $target (docker unavailable)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
chmod -R ug+rwX "$target" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
STORAGE_PATH="$(read_env STORAGE_PATH "./storage")"
|
||||
LOCAL_STORAGE_PATH="$(read_env STORAGE_PATH_LOCAL "./local-storage")"
|
||||
|
||||
declare -a targets=(
|
||||
"$STORAGE_PATH"
|
||||
"$STORAGE_PATH/modules"
|
||||
"$STORAGE_PATH/modules/.modules-meta"
|
||||
"$STORAGE_PATH/backups"
|
||||
"$STORAGE_PATH/logs"
|
||||
"$STORAGE_PATH/lua_scripts"
|
||||
"$STORAGE_PATH/install-markers"
|
||||
"$STORAGE_PATH/client-data"
|
||||
"$STORAGE_PATH/config"
|
||||
"$LOCAL_STORAGE_PATH"
|
||||
"$LOCAL_STORAGE_PATH/modules"
|
||||
"$LOCAL_STORAGE_PATH/client-data-cache"
|
||||
"$LOCAL_STORAGE_PATH/source"
|
||||
"$LOCAL_STORAGE_PATH/images"
|
||||
)
|
||||
|
||||
targets+=("${extra_paths[@]}")
|
||||
|
||||
declare -A seen=()
|
||||
for raw in "${targets[@]}"; do
|
||||
[ -n "$raw" ] || continue
|
||||
resolved="$(resolve_path "$raw")"
|
||||
if [ -n "${seen[$resolved]:-}" ]; then
|
||||
continue
|
||||
fi
|
||||
seen["$resolved"]=1
|
||||
log "🔧 Fixing permissions for $resolved"
|
||||
ensure_host_writable "$resolved"
|
||||
done
|
||||
|
||||
log "✅ Storage permissions refreshed"
|
||||
22
scripts/bash/restore-and-stage.sh
Executable file
22
scripts/bash/restore-and-stage.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# Refresh the module metadata after a database restore so runtime staging knows
|
||||
# to re-copy SQL files.
|
||||
set -euo pipefail
|
||||
|
||||
info(){ echo "🔧 [restore-stage] $*"; }
|
||||
warn(){ echo "⚠️ [restore-stage] $*" >&2; }
|
||||
|
||||
MODULES_DIR="${MODULES_DIR:-/modules}"
|
||||
MODULES_META_DIR="${MODULES_DIR}/.modules-meta"
|
||||
RESTORE_FLAG="${MODULES_META_DIR}/.restore-prestaged"
|
||||
|
||||
if [ ! -d "$MODULES_DIR" ]; then
|
||||
warn "Modules directory not found at ${MODULES_DIR}; skipping restore-time staging prep."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$MODULES_META_DIR" 2>/dev/null || true
|
||||
touch "$RESTORE_FLAG"
|
||||
echo "restore_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" > "$RESTORE_FLAG"
|
||||
|
||||
info "Flagged ${RESTORE_FLAG} to force staging on next ./scripts/bash/stage-modules.sh run."
|
||||
@@ -17,6 +17,32 @@ show_staging_step(){
|
||||
printf '%b\n' "${YELLOW}🔧 ${step}: ${message}...${NC}"
|
||||
}
|
||||
|
||||
ensure_host_writable(){
|
||||
local target="$1"
|
||||
[ -n "$target" ] || return 0
|
||||
if [ -d "$target" ] || mkdir -p "$target" 2>/dev/null; then
|
||||
local uid gid
|
||||
uid="$(id -u)"
|
||||
gid="$(id -g)"
|
||||
if ! chown -R "$uid":"$gid" "$target" 2>/dev/null; then
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
local helper_image
|
||||
helper_image="$(read_env ALPINE_IMAGE "alpine:latest")"
|
||||
docker run --rm \
|
||||
-u 0:0 \
|
||||
-v "$target":/workspace \
|
||||
"$helper_image" \
|
||||
sh -c "chown -R ${uid}:${gid} /workspace" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
chmod -R u+rwX "$target" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
seed_sql_ledger_if_needed(){
|
||||
: # No-op; ledger removed
|
||||
}
|
||||
|
||||
sync_local_staging(){
|
||||
local src_root="$LOCAL_STORAGE_PATH"
|
||||
local dest_root="$STORAGE_PATH"
|
||||
@@ -53,8 +79,21 @@ sync_local_staging(){
|
||||
return
|
||||
fi
|
||||
|
||||
# Ensure both source and destination trees are writable by the host user.
|
||||
ensure_host_writable "$src_modules"
|
||||
ensure_host_writable "$dest_modules"
|
||||
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
rsync -a --delete "$src_modules"/ "$dest_modules"/
|
||||
# rsync may return exit code 23 (permission warnings) in WSL2 - these are harmless
|
||||
rsync -a --delete "$src_modules"/ "$dest_modules"/ || {
|
||||
local rsync_exit=$?
|
||||
if [ $rsync_exit -eq 23 ]; then
|
||||
echo "ℹ️ rsync completed with permission warnings (normal in WSL2)"
|
||||
else
|
||||
echo "⚠️ rsync failed with exit code $rsync_exit"
|
||||
return $rsync_exit
|
||||
fi
|
||||
}
|
||||
else
|
||||
find "$dest_modules" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
|
||||
(cd "$src_modules" && tar cf - .) | (cd "$dest_modules" && tar xf -)
|
||||
@@ -219,7 +258,47 @@ if [[ "$LOCAL_STORAGE_PATH" != /* ]]; then
|
||||
LOCAL_STORAGE_PATH="$PROJECT_DIR/$LOCAL_STORAGE_PATH"
|
||||
fi
|
||||
LOCAL_STORAGE_PATH="$(canonical_path "$LOCAL_STORAGE_PATH")"
|
||||
STORAGE_PATH_LOCAL="$LOCAL_STORAGE_PATH"
|
||||
SENTINEL_FILE="$LOCAL_STORAGE_PATH/modules/.requires_rebuild"
|
||||
MODULES_META_DIR="$STORAGE_PATH/modules/.modules-meta"
|
||||
RESTORE_PRESTAGED_FLAG="$MODULES_META_DIR/.restore-prestaged"
|
||||
MODULES_ENABLED_FILE="$MODULES_META_DIR/modules-enabled.txt"
|
||||
MODULE_SQL_STAGE_PATH="$(read_env MODULE_SQL_STAGE_PATH "$STORAGE_PATH/module-sql-updates")"
|
||||
MODULE_SQL_STAGE_PATH="$(eval "echo \"$MODULE_SQL_STAGE_PATH\"")"
|
||||
if [[ "$MODULE_SQL_STAGE_PATH" != /* ]]; then
|
||||
MODULE_SQL_STAGE_PATH="$PROJECT_DIR/$MODULE_SQL_STAGE_PATH"
|
||||
fi
|
||||
MODULE_SQL_STAGE_PATH="$(canonical_path "$MODULE_SQL_STAGE_PATH")"
|
||||
mkdir -p "$MODULE_SQL_STAGE_PATH"
|
||||
ensure_host_writable "$MODULE_SQL_STAGE_PATH"
|
||||
HOST_STAGE_HELPER_IMAGE="$(read_env ALPINE_IMAGE "alpine:latest")"
|
||||
|
||||
declare -A ENABLED_MODULES=()
|
||||
|
||||
load_enabled_modules(){
|
||||
ENABLED_MODULES=()
|
||||
if [ -f "$MODULES_ENABLED_FILE" ]; then
|
||||
while IFS= read -r enabled_module; do
|
||||
enabled_module="$(echo "$enabled_module" | tr -d '\r')"
|
||||
[ -n "$enabled_module" ] || continue
|
||||
ENABLED_MODULES["$enabled_module"]=1
|
||||
done < "$MODULES_ENABLED_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
module_is_enabled(){
|
||||
local module_dir="$1"
|
||||
if [ ${#ENABLED_MODULES[@]} -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ -n "${ENABLED_MODULES[$module_dir]:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Load the enabled module list (if present) so staging respects disabled modules.
|
||||
load_enabled_modules
|
||||
|
||||
# Define module mappings (from rebuild-with-modules.sh)
|
||||
declare -A MODULE_REPO_MAP=(
|
||||
@@ -338,6 +417,7 @@ fi
|
||||
# Stage the services
|
||||
show_staging_step "Service Orchestration" "Preparing realm services"
|
||||
sync_local_staging
|
||||
|
||||
echo "🎬 Staging services with profile: services-$TARGET_PROFILE"
|
||||
echo "⏳ Pulling images and starting containers; this can take several minutes on first run."
|
||||
|
||||
@@ -360,10 +440,278 @@ case "$TARGET_PROFILE" in
|
||||
modules) PROFILE_ARGS+=(--profile client-data) ;;
|
||||
esac
|
||||
|
||||
# Start the target profile
|
||||
show_staging_step "Realm Activation" "Bringing services online"
|
||||
echo "🟢 Starting services-$TARGET_PROFILE profile..."
|
||||
docker compose "${PROFILE_ARGS[@]}" up -d
|
||||
# Stage module SQL to core updates directory (after containers start)
|
||||
host_stage_clear(){
|
||||
docker run --rm \
|
||||
-v "$MODULE_SQL_STAGE_PATH":/host-stage \
|
||||
"$HOST_STAGE_HELPER_IMAGE" \
|
||||
sh -c 'find /host-stage -type f -name "MODULE_*.sql" -delete' >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
host_stage_reset_dir(){
|
||||
local dir="$1"
|
||||
docker run --rm \
|
||||
-v "$MODULE_SQL_STAGE_PATH":/host-stage \
|
||||
"$HOST_STAGE_HELPER_IMAGE" \
|
||||
sh -c "mkdir -p /host-stage/$dir && rm -f /host-stage/$dir/MODULE_*.sql" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
copy_to_host_stage(){
|
||||
local file_path="$1"
|
||||
local core_dir="$2"
|
||||
local target_name="$3"
|
||||
local src_dir
|
||||
src_dir="$(dirname "$file_path")"
|
||||
local base_name
|
||||
base_name="$(basename "$file_path")"
|
||||
docker run --rm \
|
||||
-v "$MODULE_SQL_STAGE_PATH":/host-stage \
|
||||
-v "$src_dir":/src \
|
||||
"$HOST_STAGE_HELPER_IMAGE" \
|
||||
sh -c "mkdir -p /host-stage/$core_dir && cp \"/src/$base_name\" \"/host-stage/$core_dir/$target_name\"" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
stage_module_sql_to_core() {
|
||||
show_staging_step "Module SQL Staging" "Preparing module database updates"
|
||||
|
||||
# Start containers first to get access to worldserver container
|
||||
show_staging_step "Realm Activation" "Bringing services online"
|
||||
echo "🟢 Starting services-$TARGET_PROFILE profile..."
|
||||
docker compose "${PROFILE_ARGS[@]}" up -d
|
||||
|
||||
# Wait for worldserver container to be running
|
||||
echo "⏳ Waiting for worldserver container..."
|
||||
local max_wait=60
|
||||
local waited=0
|
||||
while ! docker ps --format '{{.Names}}' | grep -q "ac-worldserver" && [ $waited -lt $max_wait ]; do
|
||||
sleep 2
|
||||
waited=$((waited + 2))
|
||||
done
|
||||
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "ac-worldserver"; then
|
||||
echo "⚠️ Worldserver container not found, skipping module SQL staging"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -f "$RESTORE_PRESTAGED_FLAG" ]; then
|
||||
echo "↻ Restore pipeline detected (flag: $RESTORE_PRESTAGED_FLAG); re-staging module SQL so worldserver can apply updates."
|
||||
rm -f "$RESTORE_PRESTAGED_FLAG" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "📦 Staging module SQL files to core updates directory..."
|
||||
host_stage_clear
|
||||
|
||||
# Create core updates directories inside container
|
||||
docker exec ac-worldserver bash -c "
|
||||
mkdir -p /azerothcore/data/sql/updates/db_world \
|
||||
/azerothcore/data/sql/updates/db_characters \
|
||||
/azerothcore/data/sql/updates/db_auth
|
||||
" 2>/dev/null || true
|
||||
|
||||
# Stage SQL from all modules
|
||||
local staged_count=0
|
||||
local total_skipped=0
|
||||
local total_failed=0
|
||||
docker exec ac-worldserver bash -c "find /azerothcore/data/sql/updates -name '*_MODULE_*.sql' -delete" >/dev/null 2>&1 || true
|
||||
|
||||
shopt -s nullglob
|
||||
for db_type in db-world db-characters db-auth db-playerbots; do
|
||||
local core_dir=""
|
||||
local legacy_name=""
|
||||
case "$db_type" in
|
||||
db-world)
|
||||
core_dir="db_world"
|
||||
legacy_name="world" # Some modules use 'world' instead of 'db-world'
|
||||
;;
|
||||
db-characters)
|
||||
core_dir="db_characters"
|
||||
legacy_name="characters"
|
||||
;;
|
||||
db-auth)
|
||||
core_dir="db_auth"
|
||||
legacy_name="auth"
|
||||
;;
|
||||
db-playerbots)
|
||||
core_dir="db_playerbots"
|
||||
legacy_name="playerbots"
|
||||
;;
|
||||
esac
|
||||
|
||||
docker exec ac-worldserver bash -c "mkdir -p /azerothcore/data/sql/updates/$core_dir" >/dev/null 2>&1 || true
|
||||
host_stage_reset_dir "$core_dir"
|
||||
|
||||
local counter=0
|
||||
local skipped=0
|
||||
local failed=0
|
||||
|
||||
local search_paths=(
|
||||
"$MODULES_DIR"/*/data/sql/"$db_type"
|
||||
"$MODULES_DIR"/*/data/sql/"$db_type"/base
|
||||
"$MODULES_DIR"/*/data/sql/"$db_type"/updates
|
||||
"$MODULES_DIR"/*/data/sql/"$legacy_name"
|
||||
"$MODULES_DIR"/*/data/sql/"$legacy_name"/base
|
||||
)
|
||||
|
||||
for module_dir in "${search_paths[@]}"; do
|
||||
for sql_file in "$module_dir"/*.sql; do
|
||||
[ -e "$sql_file" ] || continue
|
||||
|
||||
if [ ! -f "$sql_file" ] || [ ! -s "$sql_file" ]; then
|
||||
echo " ⚠️ Skipped empty or invalid: $(basename "$sql_file")"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if grep -qE '^[[:space:]]*(system|exec|shell|!)' "$sql_file" 2>/dev/null; then
|
||||
echo " ❌ Security: Rejected $(basename "$(dirname "$module_dir")")/$(basename "$sql_file") (contains shell commands)"
|
||||
failed=$((failed + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
local module_name
|
||||
module_name="$(echo "$sql_file" | sed 's|.*/modules/||' | cut -d'/' -f1)"
|
||||
local base_name
|
||||
base_name="$(basename "$sql_file" .sql)"
|
||||
local update_identifier="MODULE_${module_name}_${base_name}"
|
||||
|
||||
if ! module_is_enabled "$module_name"; then
|
||||
echo " ⏭️ Skipped $module_name/$db_type/$(basename "$sql_file") (module disabled)"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
local target_name="MODULE_${module_name}_${base_name}.sql"
|
||||
if ! copy_to_host_stage "$sql_file" "$core_dir" "$target_name"; then
|
||||
echo " ❌ Failed to copy to host staging: $module_name/$db_type/$(basename "$sql_file")"
|
||||
failed=$((failed + 1))
|
||||
continue
|
||||
fi
|
||||
if docker cp "$sql_file" "ac-worldserver:/azerothcore/data/sql/updates/$core_dir/$target_name" >/dev/null; then
|
||||
echo " ✓ Staged $module_name/$db_type/$(basename "$sql_file")"
|
||||
counter=$((counter + 1))
|
||||
else
|
||||
echo " ❌ Failed to copy: $module_name/$(basename "$sql_file")"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
staged_count=$((staged_count + counter))
|
||||
total_skipped=$((total_skipped + skipped))
|
||||
total_failed=$((total_failed + failed))
|
||||
|
||||
done
|
||||
shopt -u nullglob
|
||||
|
||||
echo ""
|
||||
if [ "$staged_count" -gt 0 ]; then
|
||||
echo "✅ Staged $staged_count module SQL files to core updates directory"
|
||||
[ "$total_skipped" -gt 0 ] && echo "⚠️ Skipped $total_skipped empty/invalid file(s)"
|
||||
[ "$total_failed" -gt 0 ] && echo "❌ Failed to stage $total_failed file(s)"
|
||||
echo "🔄 Restart worldserver to apply: docker restart ac-worldserver"
|
||||
else
|
||||
echo "ℹ️ No module SQL files found to stage"
|
||||
fi
|
||||
}
|
||||
|
||||
get_module_dbc_path(){
|
||||
local module_name="$1"
|
||||
local manifest_file="$PROJECT_DIR/config/module-manifest.json"
|
||||
|
||||
if [ ! -f "$manifest_file" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
local dbc_path
|
||||
dbc_path=$(jq -r ".modules[] | select(.name == \"$module_name\") | .server_dbc_path // empty" "$manifest_file" 2>/dev/null)
|
||||
if [ -n "$dbc_path" ]; then
|
||||
echo "$dbc_path"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
stage_module_dbc_files(){
|
||||
show_staging_step "Module DBC Staging" "Deploying binary DBC files to server"
|
||||
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "ac-worldserver"; then
|
||||
echo "⚠️ Worldserver container not found, skipping module DBC staging"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "📦 Staging module DBC files to server data directory..."
|
||||
echo " (Using manifest 'server_dbc_path' field to locate server-side DBC files)"
|
||||
|
||||
local staged_count=0
|
||||
local skipped=0
|
||||
local failed=0
|
||||
|
||||
shopt -s nullglob
|
||||
for module_path in "$MODULES_DIR"/*; do
|
||||
[ -d "$module_path" ] || continue
|
||||
local module_name="$(basename "$module_path")"
|
||||
|
||||
# Skip disabled modules
|
||||
if ! module_is_enabled "$module_name"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get DBC path from manifest
|
||||
local dbc_path
|
||||
if ! dbc_path=$(get_module_dbc_path "$module_name"); then
|
||||
# No server_dbc_path defined in manifest - skip this module
|
||||
continue
|
||||
fi
|
||||
|
||||
local dbc_dir="$module_path/$dbc_path"
|
||||
if [ ! -d "$dbc_dir" ]; then
|
||||
echo " ⚠️ $module_name: DBC directory not found at $dbc_path"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
for dbc_file in "$dbc_dir"/*.dbc; do
|
||||
[ -e "$dbc_file" ] || continue
|
||||
|
||||
if [ ! -f "$dbc_file" ] || [ ! -s "$dbc_file" ]; then
|
||||
echo " ⚠️ Skipped empty or invalid: $module_name/$(basename "$dbc_file")"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
local dbc_filename="$(basename "$dbc_file")"
|
||||
|
||||
# Copy to worldserver DBC directory
|
||||
if docker cp "$dbc_file" "ac-worldserver:/azerothcore/data/dbc/$dbc_filename" >/dev/null 2>&1; then
|
||||
echo " ✓ Staged $module_name → $dbc_filename"
|
||||
staged_count=$((staged_count + 1))
|
||||
else
|
||||
echo " ❌ Failed to copy: $module_name/$dbc_filename"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done
|
||||
done
|
||||
shopt -u nullglob
|
||||
|
||||
echo ""
|
||||
if [ "$staged_count" -gt 0 ]; then
|
||||
echo "✅ Staged $staged_count module DBC files to server data directory"
|
||||
[ "$skipped" -gt 0 ] && echo "⚠️ Skipped $skipped file(s) (no server_dbc_path in manifest)"
|
||||
[ "$failed" -gt 0 ] && echo "❌ Failed to stage $failed file(s)"
|
||||
echo "🔄 Restart worldserver to load new DBC data: docker restart ac-worldserver"
|
||||
else
|
||||
echo "ℹ️ No module DBC files found to stage (use 'server_dbc_path' in manifest to enable)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Stage module SQL (this will also start the containers)
|
||||
stage_module_sql_to_core
|
||||
|
||||
# Stage module DBC files
|
||||
stage_module_dbc_files
|
||||
|
||||
printf '\n%b\n' "${GREEN}⚔️ Realm staging completed successfully! ⚔️${NC}"
|
||||
printf '%b\n' "${GREEN}🏰 Profile: services-$TARGET_PROFILE${NC}"
|
||||
|
||||
293
scripts/bash/statusjson.sh
Executable file
293
scripts/bash/statusjson.sh
Executable file
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_DIR = Path(__file__).resolve().parents[2]
|
||||
ENV_FILE = PROJECT_DIR / ".env"
|
||||
|
||||
def load_env():
|
||||
env = {}
|
||||
if ENV_FILE.exists():
|
||||
for line in ENV_FILE.read_text().splitlines():
|
||||
if not line or line.strip().startswith('#'):
|
||||
continue
|
||||
if '=' not in line:
|
||||
continue
|
||||
key, val = line.split('=', 1)
|
||||
val = val.split('#', 1)[0].strip()
|
||||
env[key.strip()] = val
|
||||
return env
|
||||
|
||||
def read_env(env, key, default=""):
|
||||
return env.get(key, default)
|
||||
|
||||
def docker_exists(name):
|
||||
result = subprocess.run([
|
||||
"docker", "ps", "-a", "--format", "{{.Names}}"
|
||||
], capture_output=True, text=True)
|
||||
names = set(result.stdout.split())
|
||||
return name in names
|
||||
|
||||
def docker_inspect(name, template):
|
||||
try:
|
||||
result = subprocess.run([
|
||||
"docker", "inspect", f"--format={template}", name
|
||||
], capture_output=True, text=True, check=True)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError:
|
||||
return ""
|
||||
|
||||
def service_snapshot(name, label):
|
||||
status = "missing"
|
||||
health = "none"
|
||||
started = ""
|
||||
image = ""
|
||||
exit_code = ""
|
||||
if docker_exists(name):
|
||||
status = docker_inspect(name, "{{.State.Status}}") or status
|
||||
health = docker_inspect(name, "{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}") or health
|
||||
started = docker_inspect(name, "{{.State.StartedAt}}") or ""
|
||||
image = docker_inspect(name, "{{.Config.Image}}") or ""
|
||||
exit_code = docker_inspect(name, "{{.State.ExitCode}}") or "0"
|
||||
return {
|
||||
"name": name,
|
||||
"label": label,
|
||||
"status": status,
|
||||
"health": health,
|
||||
"started_at": started,
|
||||
"image": image,
|
||||
"exit_code": exit_code,
|
||||
}
|
||||
|
||||
def port_reachable(port):
|
||||
if not port:
|
||||
return False
|
||||
try:
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
return False
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=1):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def module_list(env):
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Load module manifest
|
||||
manifest_path = PROJECT_DIR / "config" / "module-manifest.json"
|
||||
manifest_map = {}
|
||||
if manifest_path.exists():
|
||||
try:
|
||||
manifest_data = json.loads(manifest_path.read_text())
|
||||
for mod in manifest_data.get("modules", []):
|
||||
manifest_map[mod["key"]] = mod
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
modules = []
|
||||
pattern = re.compile(r"^MODULE_([A-Z0-9_]+)=1$")
|
||||
if ENV_FILE.exists():
|
||||
for line in ENV_FILE.read_text().splitlines():
|
||||
m = pattern.match(line.strip())
|
||||
if m:
|
||||
key = "MODULE_" + m.group(1)
|
||||
raw = m.group(1).lower().replace('_', ' ')
|
||||
title = raw.title()
|
||||
|
||||
# Look up manifest info
|
||||
mod_info = manifest_map.get(key, {})
|
||||
modules.append({
|
||||
"name": title,
|
||||
"key": key,
|
||||
"description": mod_info.get("description", "No description available"),
|
||||
"category": mod_info.get("category", "unknown"),
|
||||
"type": mod_info.get("type", "unknown")
|
||||
})
|
||||
return modules
|
||||
|
||||
def dir_info(path):
|
||||
p = Path(path)
|
||||
exists = p.exists()
|
||||
size = "--"
|
||||
if exists:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["du", "-sh", str(p)],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.stdout:
|
||||
size = result.stdout.split()[0]
|
||||
except Exception:
|
||||
size = "--"
|
||||
return {"path": str(p), "exists": exists, "size": size}
|
||||
|
||||
def volume_info(name, fallback=None):
|
||||
candidates = [name]
|
||||
if fallback:
|
||||
candidates.append(fallback)
|
||||
for cand in candidates:
|
||||
result = subprocess.run(["docker", "volume", "inspect", cand], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
try:
|
||||
data = json.loads(result.stdout)[0]
|
||||
return {
|
||||
"name": cand,
|
||||
"exists": True,
|
||||
"mountpoint": data.get("Mountpoint", "-")
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return {"name": name, "exists": False, "mountpoint": "-"}
|
||||
|
||||
def expand_path(value, env):
|
||||
storage = read_env(env, "STORAGE_PATH", "./storage")
|
||||
local_storage = read_env(env, "STORAGE_PATH_LOCAL", "./local-storage")
|
||||
value = value.replace('${STORAGE_PATH}', storage)
|
||||
value = value.replace('${STORAGE_PATH_LOCAL}', local_storage)
|
||||
return value
|
||||
|
||||
def mysql_query(env, database, query):
|
||||
password = read_env(env, "MYSQL_ROOT_PASSWORD")
|
||||
user = read_env(env, "MYSQL_USER", "root")
|
||||
if not password or not database:
|
||||
return 0
|
||||
cmd = [
|
||||
"docker", "exec", "ac-mysql",
|
||||
"mysql", "-N", "-B",
|
||||
f"-u{user}", f"-p{password}", database,
|
||||
"-e", query
|
||||
]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
value = result.stdout.strip().splitlines()[-1]
|
||||
return int(value)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def user_stats(env):
|
||||
db_auth = read_env(env, "DB_AUTH_NAME", "acore_auth")
|
||||
db_characters = read_env(env, "DB_CHARACTERS_NAME", "acore_characters")
|
||||
accounts = mysql_query(env, db_auth, "SELECT COUNT(*) FROM account;")
|
||||
online = mysql_query(env, db_auth, "SELECT COUNT(*) FROM account WHERE online = 1;")
|
||||
active = mysql_query(env, db_auth, "SELECT COUNT(*) FROM account WHERE last_login >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 7 DAY);")
|
||||
characters = mysql_query(env, db_characters, "SELECT COUNT(*) FROM characters;")
|
||||
return {
|
||||
"accounts": accounts,
|
||||
"online": online,
|
||||
"characters": characters,
|
||||
"active7d": active,
|
||||
}
|
||||
|
||||
def docker_stats():
|
||||
"""Get CPU and memory stats for running containers"""
|
||||
try:
|
||||
result = subprocess.run([
|
||||
"docker", "stats", "--no-stream", "--no-trunc",
|
||||
"--format", "{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
|
||||
], capture_output=True, text=True, check=True, timeout=4)
|
||||
|
||||
stats = {}
|
||||
for line in result.stdout.strip().splitlines():
|
||||
parts = line.split('\t')
|
||||
if len(parts) == 4:
|
||||
name, cpu, mem_usage, mem_perc = parts
|
||||
# Parse CPU percentage (e.g., "0.50%" -> 0.50)
|
||||
cpu_val = cpu.replace('%', '').strip()
|
||||
try:
|
||||
cpu_float = float(cpu_val)
|
||||
except ValueError:
|
||||
cpu_float = 0.0
|
||||
|
||||
# Parse memory percentage
|
||||
mem_perc_val = mem_perc.replace('%', '').strip()
|
||||
try:
|
||||
mem_perc_float = float(mem_perc_val)
|
||||
except ValueError:
|
||||
mem_perc_float = 0.0
|
||||
|
||||
stats[name] = {
|
||||
"cpu": cpu_float,
|
||||
"memory": mem_usage.strip(),
|
||||
"memory_percent": mem_perc_float
|
||||
}
|
||||
return stats
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def main():
|
||||
env = load_env()
|
||||
project = read_env(env, "COMPOSE_PROJECT_NAME", "acore-compose")
|
||||
network = read_env(env, "NETWORK_NAME", "azerothcore")
|
||||
|
||||
services = [
|
||||
("ac-mysql", "MySQL"),
|
||||
("ac-backup", "Backup"),
|
||||
("ac-volume-init", "Volume Init"),
|
||||
("ac-storage-init", "Storage Init"),
|
||||
("ac-db-init", "DB Init"),
|
||||
("ac-db-import", "DB Import"),
|
||||
("ac-authserver", "Auth Server"),
|
||||
("ac-worldserver", "World Server"),
|
||||
("ac-client-data", "Client Data"),
|
||||
("ac-modules", "Module Manager"),
|
||||
("ac-post-install", "Post Install"),
|
||||
("ac-phpmyadmin", "phpMyAdmin"),
|
||||
("ac-keira3", "Keira3"),
|
||||
]
|
||||
|
||||
service_data = [service_snapshot(name, label) for name, label in services]
|
||||
|
||||
port_entries = [
|
||||
{"name": "Auth", "port": read_env(env, "AUTH_EXTERNAL_PORT"), "reachable": port_reachable(read_env(env, "AUTH_EXTERNAL_PORT"))},
|
||||
{"name": "World", "port": read_env(env, "WORLD_EXTERNAL_PORT"), "reachable": port_reachable(read_env(env, "WORLD_EXTERNAL_PORT"))},
|
||||
{"name": "SOAP", "port": read_env(env, "SOAP_EXTERNAL_PORT"), "reachable": port_reachable(read_env(env, "SOAP_EXTERNAL_PORT"))},
|
||||
{"name": "MySQL", "port": read_env(env, "MYSQL_EXTERNAL_PORT"), "reachable": port_reachable(read_env(env, "MYSQL_EXTERNAL_PORT")) if read_env(env, "COMPOSE_OVERRIDE_MYSQL_EXPOSE_ENABLED", "0") == "1" else False},
|
||||
{"name": "phpMyAdmin", "port": read_env(env, "PMA_EXTERNAL_PORT"), "reachable": port_reachable(read_env(env, "PMA_EXTERNAL_PORT"))},
|
||||
{"name": "Keira3", "port": read_env(env, "KEIRA3_EXTERNAL_PORT"), "reachable": port_reachable(read_env(env, "KEIRA3_EXTERNAL_PORT"))},
|
||||
]
|
||||
|
||||
storage_path = expand_path(read_env(env, "STORAGE_PATH", "./storage"), env)
|
||||
local_storage_path = expand_path(read_env(env, "STORAGE_PATH_LOCAL", "./local-storage"), env)
|
||||
client_data_path = expand_path(read_env(env, "CLIENT_DATA_PATH", f"{storage_path}/client-data"), env)
|
||||
|
||||
storage_info = {
|
||||
"storage": dir_info(storage_path),
|
||||
"local_storage": dir_info(local_storage_path),
|
||||
"client_data": dir_info(client_data_path),
|
||||
"modules": dir_info(os.path.join(storage_path, "modules")),
|
||||
"local_modules": dir_info(os.path.join(local_storage_path, "modules")),
|
||||
}
|
||||
|
||||
volumes = {
|
||||
"client_cache": volume_info(f"{project}_client-data-cache"),
|
||||
"mysql_data": volume_info(f"{project}_mysql-data", "mysql-data"),
|
||||
}
|
||||
|
||||
data = {
|
||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"project": project,
|
||||
"network": network,
|
||||
"services": service_data,
|
||||
"ports": port_entries,
|
||||
"modules": module_list(env),
|
||||
"storage": storage_info,
|
||||
"volumes": volumes,
|
||||
"users": user_stats(env),
|
||||
"stats": docker_stats(),
|
||||
}
|
||||
|
||||
print(json.dumps(data))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
342
scripts/bash/test-phase1-integration.sh
Executable file
342
scripts/bash/test-phase1-integration.sh
Executable file
@@ -0,0 +1,342 @@
|
||||
#!/bin/bash
|
||||
# Phase 1 Integration Test Script
|
||||
# Tests the complete Phase 1 implementation using build and deploy workflows
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
# Colors
|
||||
BLUE='\033[0;34m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Icons
|
||||
ICON_SUCCESS="✅"
|
||||
ICON_WARNING="⚠️"
|
||||
ICON_ERROR="❌"
|
||||
ICON_INFO="ℹ️"
|
||||
ICON_TEST="🧪"
|
||||
|
||||
# Counters
|
||||
TESTS_TOTAL=0
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
|
||||
info() {
|
||||
echo -e "${BLUE}${ICON_INFO}${NC} $*"
|
||||
}
|
||||
|
||||
ok() {
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} $*"
|
||||
((TESTS_PASSED+=1))
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo -e "${YELLOW}${ICON_WARNING}${NC} $*"
|
||||
}
|
||||
|
||||
err() {
|
||||
echo -e "${RED}${ICON_ERROR}${NC} $*"
|
||||
((TESTS_FAILED+=1))
|
||||
}
|
||||
|
||||
test_header() {
|
||||
((TESTS_TOTAL+=1))
|
||||
echo ""
|
||||
echo -e "${BOLD}${ICON_TEST} Test $TESTS_TOTAL: $*${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
}
|
||||
|
||||
section_header() {
|
||||
echo ""
|
||||
echo ""
|
||||
echo -e "${BOLD}${BLUE}═══════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${BOLD}${BLUE} $*${NC}"
|
||||
echo -e "${BOLD}${BLUE}═══════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Change to project root
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
section_header "Phase 1 Integration Test Suite"
|
||||
|
||||
info "Project root: $PROJECT_ROOT"
|
||||
info "Test started: $(date)"
|
||||
|
||||
# Ensure storage directories are writable before generating module state
|
||||
if [ -x "$PROJECT_ROOT/scripts/bash/repair-storage-permissions.sh" ]; then
|
||||
info "Normalizing storage permissions"
|
||||
"$PROJECT_ROOT/scripts/bash/repair-storage-permissions.sh" --silent || true
|
||||
fi
|
||||
|
||||
# Test 1: Verify .env exists
|
||||
test_header "Environment Configuration Check"
|
||||
if [ -f .env ]; then
|
||||
ok ".env file exists"
|
||||
|
||||
# Count enabled modules
|
||||
enabled_count=$(grep -c "^MODULE_.*=1" .env || echo "0")
|
||||
info "Enabled modules: $enabled_count"
|
||||
|
||||
# Check for playerbots
|
||||
if grep -q "^MODULE_PLAYERBOTS=1" .env; then
|
||||
info "Playerbots module enabled"
|
||||
fi
|
||||
else
|
||||
err ".env file not found"
|
||||
echo "Please run ./setup.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Module manifest validation
|
||||
test_header "Module Manifest Validation"
|
||||
if [ -f config/module-manifest.json ]; then
|
||||
ok "Module manifest exists"
|
||||
|
||||
# Validate JSON
|
||||
if python3 -m json.tool config/module-manifest.json >/dev/null 2>&1; then
|
||||
ok "Module manifest is valid JSON"
|
||||
else
|
||||
err "Module manifest has invalid JSON"
|
||||
fi
|
||||
else
|
||||
err "Module manifest not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 3: Generate module state with SQL discovery
|
||||
test_header "Module State Generation (SQL Discovery)"
|
||||
info "Running: python3 scripts/python/modules.py generate"
|
||||
|
||||
if python3 scripts/python/modules.py \
|
||||
--env-path .env \
|
||||
--manifest config/module-manifest.json \
|
||||
generate --output-dir local-storage/modules > /tmp/phase1-modules-generate.log 2>&1; then
|
||||
ok "Module state generation successful"
|
||||
else
|
||||
# Check if it's just warnings
|
||||
if grep -q "warnings detected" /tmp/phase1-modules-generate.log 2>/dev/null; then
|
||||
ok "Module state generation completed with warnings"
|
||||
else
|
||||
err "Module state generation failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 4: Verify SQL manifest created
|
||||
test_header "SQL Manifest Verification"
|
||||
if [ -f local-storage/modules/.sql-manifest.json ]; then
|
||||
ok "SQL manifest created: local-storage/modules/.sql-manifest.json"
|
||||
|
||||
# Check manifest structure
|
||||
module_count=$(python3 -c "import json; data=json.load(open('local-storage/modules/.sql-manifest.json')); print(len(data.get('modules', [])))" 2>/dev/null || echo "0")
|
||||
info "Modules with SQL: $module_count"
|
||||
|
||||
if [ "$module_count" -gt 0 ]; then
|
||||
ok "SQL manifest contains $module_count module(s)"
|
||||
|
||||
# Show first module
|
||||
info "Sample module SQL info:"
|
||||
python3 -c "import json; data=json.load(open('local-storage/modules/.sql-manifest.json')); m=data['modules'][0] if data['modules'] else {}; print(f\" Name: {m.get('name', 'N/A')}\n SQL files: {len(m.get('sql_files', {}))}\") " 2>/dev/null || true
|
||||
else
|
||||
warn "No modules with SQL files (expected if modules not yet staged)"
|
||||
fi
|
||||
else
|
||||
err "SQL manifest not created"
|
||||
fi
|
||||
|
||||
# Test 5: Verify modules.env created
|
||||
test_header "Module Environment File Check"
|
||||
if [ -f local-storage/modules/modules.env ]; then
|
||||
ok "modules.env created"
|
||||
|
||||
# Check for key exports
|
||||
if grep -q "MODULES_ENABLED=" local-storage/modules/modules.env; then
|
||||
ok "MODULES_ENABLED variable present"
|
||||
fi
|
||||
|
||||
if grep -q "MODULES_REQUIRES_CUSTOM_BUILD=" local-storage/modules/modules.env; then
|
||||
ok "Build requirement flags present"
|
||||
|
||||
# Check if build required
|
||||
source local-storage/modules/modules.env
|
||||
if [ "${MODULES_REQUIRES_CUSTOM_BUILD:-0}" = "1" ]; then
|
||||
info "Custom build required (C++ modules enabled)"
|
||||
else
|
||||
info "Standard build sufficient (no C++ modules)"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
err "modules.env not created"
|
||||
fi
|
||||
|
||||
# Test 6: Check build requirement
|
||||
test_header "Build Requirement Check"
|
||||
if [ -f local-storage/modules/modules.env ]; then
|
||||
source local-storage/modules/modules.env
|
||||
|
||||
info "MODULES_REQUIRES_CUSTOM_BUILD=${MODULES_REQUIRES_CUSTOM_BUILD:-0}"
|
||||
info "MODULES_REQUIRES_PLAYERBOT_SOURCE=${MODULES_REQUIRES_PLAYERBOT_SOURCE:-0}"
|
||||
|
||||
if [ "${MODULES_REQUIRES_CUSTOM_BUILD:-0}" = "1" ]; then
|
||||
ok "Build system correctly detected C++ modules"
|
||||
BUILD_REQUIRED=1
|
||||
else
|
||||
ok "Build system correctly detected no C++ modules"
|
||||
BUILD_REQUIRED=0
|
||||
fi
|
||||
else
|
||||
warn "Cannot determine build requirements"
|
||||
BUILD_REQUIRED=0
|
||||
fi
|
||||
|
||||
# Test 7: Verify new scripts exist and are executable
|
||||
test_header "New Script Verification"
|
||||
scripts=(
|
||||
"scripts/bash/verify-sql-updates.sh"
|
||||
"scripts/bash/backup-status.sh"
|
||||
"scripts/bash/db-health-check.sh"
|
||||
)
|
||||
|
||||
for script in "${scripts[@]}"; do
|
||||
if [ -f "$script" ]; then
|
||||
if [ -x "$script" ]; then
|
||||
ok "$(basename "$script") - exists and executable"
|
||||
else
|
||||
warn "$(basename "$script") - exists but not executable"
|
||||
chmod +x "$script"
|
||||
ok "Fixed permissions for $(basename "$script")"
|
||||
fi
|
||||
else
|
||||
err "$(basename "$script") - not found"
|
||||
fi
|
||||
done
|
||||
|
||||
# Test 8: Test backup-status.sh (without running containers)
|
||||
test_header "Backup Status Script Test"
|
||||
backup_status_log="$(mktemp)"
|
||||
if ./scripts/bash/backup-status.sh >"$backup_status_log" 2>&1; then
|
||||
if grep -q "BACKUP STATUS" "$backup_status_log"; then
|
||||
ok "backup-status.sh executes successfully"
|
||||
else
|
||||
err "backup-status.sh output missing 'BACKUP STATUS' marker"
|
||||
fi
|
||||
else
|
||||
err "backup-status.sh failed to execute"
|
||||
fi
|
||||
rm -f "$backup_status_log"
|
||||
|
||||
# Test 9: Test db-health-check.sh help
|
||||
test_header "Database Health Check Script Test"
|
||||
if ./scripts/bash/db-health-check.sh --help | grep -q "Check the health status"; then
|
||||
ok "db-health-check.sh help working"
|
||||
else
|
||||
err "db-health-check.sh help failed"
|
||||
fi
|
||||
|
||||
# Test 10: Check modified scripts for new functionality
|
||||
test_header "Modified Script Verification"
|
||||
|
||||
# Check stage-modules.sh has runtime SQL staging function
|
||||
if grep -q "stage_module_sql_to_core()" scripts/bash/stage-modules.sh; then
|
||||
ok "stage-modules.sh contains runtime SQL staging function"
|
||||
else
|
||||
err "stage-modules.sh missing runtime SQL staging function"
|
||||
fi
|
||||
|
||||
# Check db-import-conditional.sh has playerbots support
|
||||
if grep -q "PlayerbotsDatabaseInfo" scripts/bash/db-import-conditional.sh; then
|
||||
ok "db-import-conditional.sh has playerbots database support"
|
||||
else
|
||||
err "db-import-conditional.sh missing playerbots support"
|
||||
fi
|
||||
|
||||
if grep -q "Updates.EnableDatabases = 15" scripts/bash/db-import-conditional.sh; then
|
||||
ok "db-import-conditional.sh has correct EnableDatabases value (15)"
|
||||
else
|
||||
warn "db-import-conditional.sh may have incorrect EnableDatabases value"
|
||||
fi
|
||||
|
||||
# Check for restore marker safety net
|
||||
if grep -q "verify_databases_populated" scripts/bash/db-import-conditional.sh; then
|
||||
ok "db-import-conditional.sh verifies live MySQL state before honoring restore markers"
|
||||
else
|
||||
err "db-import-conditional.sh missing restore marker safety check"
|
||||
fi
|
||||
|
||||
# Check for post-restore verification
|
||||
if grep -q "verify_and_update_restored_databases" scripts/bash/db-import-conditional.sh; then
|
||||
ok "db-import-conditional.sh has post-restore verification"
|
||||
else
|
||||
err "db-import-conditional.sh missing post-restore verification"
|
||||
fi
|
||||
|
||||
# Test 11: Restore + Module Staging Automation
|
||||
test_header "Restore + Module Staging Automation"
|
||||
if grep -q "restore-and-stage.sh" docker-compose.yml && \
|
||||
grep -q ".restore-prestaged" scripts/bash/restore-and-stage.sh; then
|
||||
ok "restore-and-stage.sh wired into compose and flags stage-modules to recopy SQL"
|
||||
else
|
||||
err "restore-and-stage.sh missing compose wiring or flag handling"
|
||||
fi
|
||||
|
||||
# Test 12: Docker Compose configuration check
|
||||
test_header "Docker Compose Configuration Check"
|
||||
if [ -f docker-compose.yml ]; then
|
||||
ok "docker-compose.yml exists"
|
||||
|
||||
# Check for required services
|
||||
if grep -q "ac-mysql:" docker-compose.yml; then
|
||||
ok "MySQL service configured"
|
||||
fi
|
||||
|
||||
if grep -q "ac-worldserver:" docker-compose.yml; then
|
||||
ok "Worldserver service configured"
|
||||
fi
|
||||
else
|
||||
err "docker-compose.yml not found"
|
||||
fi
|
||||
|
||||
# Test Summary
|
||||
section_header "Test Summary"
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Tests Executed: $TESTS_TOTAL${NC}"
|
||||
echo -e "${GREEN}${BOLD}Passed: $TESTS_PASSED${NC}"
|
||||
if [ $TESTS_FAILED -gt 0 ]; then
|
||||
echo -e "${RED}${BOLD}Failed: $TESTS_FAILED${NC}"
|
||||
else
|
||||
echo -e "${GREEN}${BOLD}Failed: $TESTS_FAILED${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Calculate success rate
|
||||
if [ $TESTS_TOTAL -gt 0 ]; then
|
||||
success_rate=$((TESTS_PASSED * 100 / TESTS_TOTAL))
|
||||
echo -e "${BOLD}Success Rate: ${success_rate}%${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
if [ $TESTS_FAILED -eq 0 ]; then
|
||||
echo -e "${GREEN}${BOLD}${ICON_SUCCESS} ALL TESTS PASSED${NC}"
|
||||
echo ""
|
||||
echo "Phase 1 implementation is working correctly!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Run './build.sh' if C++ modules are enabled"
|
||||
echo " 2. Run './deploy.sh' to start containers"
|
||||
echo " 3. Verify SQL staging with running containers"
|
||||
echo " 4. Check database health with db-health-check.sh"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}${BOLD}${ICON_ERROR} SOME TESTS FAILED${NC}"
|
||||
echo ""
|
||||
echo "Please review the failures above before proceeding."
|
||||
exit 1
|
||||
fi
|
||||
348
scripts/bash/verify-sql-updates.sh
Executable file
348
scripts/bash/verify-sql-updates.sh
Executable file
@@ -0,0 +1,348 @@
|
||||
#!/bin/bash
|
||||
# Verify SQL Updates
|
||||
# Checks that SQL updates have been applied via the updates table
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Icons
|
||||
ICON_SUCCESS="✅"
|
||||
ICON_WARNING="⚠️"
|
||||
ICON_ERROR="❌"
|
||||
ICON_INFO="ℹ️"
|
||||
|
||||
# Default values
|
||||
MODULE_NAME=""
|
||||
DATABASE_NAME=""
|
||||
SHOW_ALL=0
|
||||
CHECK_HASH=0
|
||||
CONTAINER_NAME="ac-mysql"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./verify-sql-updates.sh [options]
|
||||
|
||||
Verify that SQL updates have been applied via AzerothCore's updates table.
|
||||
|
||||
Options:
|
||||
--module NAME Check specific module
|
||||
--database NAME Check specific database (auth/world/characters)
|
||||
--all Show all module updates
|
||||
--check-hash Verify file hashes match database
|
||||
--container NAME MySQL container name (default: ac-mysql)
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
./verify-sql-updates.sh --all
|
||||
./verify-sql-updates.sh --module mod-aoe-loot
|
||||
./verify-sql-updates.sh --database acore_world --all
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--module) MODULE_NAME="$2"; shift 2;;
|
||||
--database) DATABASE_NAME="$2"; shift 2;;
|
||||
--all) SHOW_ALL=1; shift;;
|
||||
--check-hash) CHECK_HASH=1; shift;;
|
||||
--container) CONTAINER_NAME="$2"; shift 2;;
|
||||
-h|--help) usage; exit 0;;
|
||||
*) echo "Unknown option: $1"; usage; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Load environment
|
||||
if [ -f "$PROJECT_ROOT/.env" ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$PROJECT_ROOT/.env"
|
||||
set +a
|
||||
fi
|
||||
|
||||
MYSQL_HOST="${MYSQL_HOST:-ac-mysql}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
MYSQL_USER="${MYSQL_USER:-root}"
|
||||
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}"
|
||||
DB_AUTH_NAME="${DB_AUTH_NAME:-acore_auth}"
|
||||
DB_WORLD_NAME="${DB_WORLD_NAME:-acore_world}"
|
||||
DB_CHARACTERS_NAME="${DB_CHARACTERS_NAME:-acore_characters}"
|
||||
DB_PLAYERBOTS_NAME="${DB_PLAYERBOTS_NAME:-acore_playerbots}"
|
||||
|
||||
# Logging functions
|
||||
info() {
|
||||
echo -e "${BLUE}${ICON_INFO}${NC} $*"
|
||||
}
|
||||
|
||||
ok() {
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} $*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo -e "${YELLOW}${ICON_WARNING}${NC} $*"
|
||||
}
|
||||
|
||||
err() {
|
||||
echo -e "${RED}${ICON_ERROR}${NC} $*"
|
||||
}
|
||||
|
||||
# MySQL query helper
|
||||
mysql_query() {
|
||||
local database="${1:-}"
|
||||
local query="$2"
|
||||
|
||||
if [ -z "$MYSQL_ROOT_PASSWORD" ]; then
|
||||
err "MYSQL_ROOT_PASSWORD not set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
if [ -n "$database" ]; then
|
||||
docker exec "$CONTAINER_NAME" mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_ROOT_PASSWORD" "$database" -N -B -e "$query" 2>/dev/null
|
||||
else
|
||||
docker exec "$CONTAINER_NAME" mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_ROOT_PASSWORD" -N -B -e "$query" 2>/dev/null
|
||||
fi
|
||||
else
|
||||
if [ -n "$database" ]; then
|
||||
mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_ROOT_PASSWORD" "$database" -N -B -e "$query" 2>/dev/null
|
||||
else
|
||||
mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_ROOT_PASSWORD" -N -B -e "$query" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if database exists
|
||||
db_exists() {
|
||||
local db_name="$1"
|
||||
local count
|
||||
count=$(mysql_query "" "SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$db_name'" 2>/dev/null || echo "0")
|
||||
[ "$count" = "1" ]
|
||||
}
|
||||
|
||||
# Verify module SQL in database
|
||||
verify_module_sql() {
|
||||
local module_name="$1"
|
||||
local database_name="$2"
|
||||
|
||||
if ! db_exists "$database_name"; then
|
||||
err "Database does not exist: $database_name"
|
||||
return 1
|
||||
fi
|
||||
|
||||
info "Checking module updates in $database_name"
|
||||
|
||||
# Query updates table for module
|
||||
local query="SELECT name, hash, state, timestamp, speed FROM updates WHERE name LIKE '%${module_name}%' AND state='MODULE' ORDER BY timestamp DESC"
|
||||
local results
|
||||
results=$(mysql_query "$database_name" "$query" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$results" ]; then
|
||||
warn "No updates found for module: $module_name in $database_name"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Display results
|
||||
echo
|
||||
printf "${BOLD}${CYAN}Module Updates for %s in %s:${NC}\n" "$module_name" "$database_name"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
while IFS=$'\t' read -r name hash state timestamp speed; do
|
||||
printf "${GREEN}${ICON_SUCCESS}${NC} %s\n" "$name"
|
||||
printf " Hash: %s\n" "${hash:0:12}..."
|
||||
printf " Applied: %s\n" "$timestamp"
|
||||
printf " Speed: %sms\n" "$speed"
|
||||
echo
|
||||
done <<< "$results"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# List all module updates
|
||||
list_module_updates() {
|
||||
local database_name="$1"
|
||||
|
||||
if ! db_exists "$database_name"; then
|
||||
err "Database does not exist: $database_name"
|
||||
return 1
|
||||
fi
|
||||
|
||||
info "Listing all module updates in $database_name"
|
||||
|
||||
# Query all module updates
|
||||
local query="SELECT name, state, timestamp FROM updates WHERE state='MODULE' ORDER BY timestamp DESC"
|
||||
local results
|
||||
results=$(mysql_query "$database_name" "$query" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$results" ]; then
|
||||
warn "No module updates found in $database_name"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Display results
|
||||
echo
|
||||
printf "${BOLD}${CYAN}All Module Updates in %s:${NC}\n" "$database_name"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
local count=0
|
||||
while IFS=$'\t' read -r name state timestamp; do
|
||||
printf "${GREEN}${ICON_SUCCESS}${NC} %s\n" "$name"
|
||||
printf " Applied: %s\n" "$timestamp"
|
||||
((count++))
|
||||
done <<< "$results"
|
||||
|
||||
echo
|
||||
ok "Total module updates: $count"
|
||||
echo
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check update applied
|
||||
check_update_applied() {
|
||||
local filename="$1"
|
||||
local database_name="$2"
|
||||
local expected_hash="${3:-}"
|
||||
|
||||
if ! db_exists "$database_name"; then
|
||||
err "Database does not exist: $database_name"
|
||||
return 2
|
||||
fi
|
||||
|
||||
# Query for specific file
|
||||
local query="SELECT hash, state, timestamp FROM updates WHERE name='$filename' LIMIT 1"
|
||||
local result
|
||||
result=$(mysql_query "$database_name" "$query" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$result" ]; then
|
||||
warn "Update not found: $filename"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse result
|
||||
IFS=$'\t' read -r hash state timestamp <<< "$result"
|
||||
|
||||
ok "Update applied: $filename"
|
||||
printf " Hash: %s\n" "$hash"
|
||||
printf " State: %s\n" "$state"
|
||||
printf " Applied: %s\n" "$timestamp"
|
||||
|
||||
# Check hash if provided
|
||||
if [ -n "$expected_hash" ] && [ "$expected_hash" != "$hash" ]; then
|
||||
err "Hash mismatch!"
|
||||
printf " Expected: %s\n" "$expected_hash"
|
||||
printf " Actual: %s\n" "$hash"
|
||||
return 2
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Generate verification report
|
||||
generate_verification_report() {
|
||||
echo
|
||||
printf "${BOLD}${BLUE}🔍 Module SQL Verification Report${NC}\n"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo
|
||||
|
||||
local total_updates=0
|
||||
local databases=("$DB_AUTH_NAME" "$DB_WORLD_NAME" "$DB_CHARACTERS_NAME")
|
||||
|
||||
# Add playerbots if it exists
|
||||
if db_exists "$DB_PLAYERBOTS_NAME"; then
|
||||
databases+=("$DB_PLAYERBOTS_NAME")
|
||||
fi
|
||||
|
||||
for db in "${databases[@]}"; do
|
||||
if ! db_exists "$db"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get count of module updates
|
||||
local count
|
||||
count=$(mysql_query "$db" "SELECT COUNT(*) FROM updates WHERE state='MODULE'" 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$count" != "0" ]; then
|
||||
printf "${GREEN}${ICON_SUCCESS}${NC} ${BOLD}%s:${NC} %s module update(s)\n" "$db" "$count"
|
||||
total_updates=$((total_updates + count))
|
||||
|
||||
if [ "$SHOW_ALL" = "1" ]; then
|
||||
# Show recent updates
|
||||
local query="SELECT name, timestamp FROM updates WHERE state='MODULE' ORDER BY timestamp DESC LIMIT 5"
|
||||
local results
|
||||
results=$(mysql_query "$db" "$query" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$results" ]; then
|
||||
while IFS=$'\t' read -r name timestamp; do
|
||||
printf " - %s (%s)\n" "$name" "$timestamp"
|
||||
done <<< "$results"
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
else
|
||||
printf "${YELLOW}${ICON_WARNING}${NC} ${BOLD}%s:${NC} No module updates\n" "$db"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
printf "${BOLD}Total: %s module update(s) applied${NC}\n" "$total_updates"
|
||||
echo
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
echo
|
||||
info "SQL Update Verification"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo
|
||||
|
||||
# Test MySQL connection
|
||||
if ! mysql_query "" "SELECT 1" >/dev/null 2>&1; then
|
||||
err "Cannot connect to MySQL server"
|
||||
printf " Host: %s:%s\n" "$MYSQL_HOST" "$MYSQL_PORT"
|
||||
printf " User: %s\n" "$MYSQL_USER"
|
||||
printf " Container: %s\n\n" "$CONTAINER_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Execute based on options
|
||||
if [ -n "$MODULE_NAME" ]; then
|
||||
# Check specific module
|
||||
if [ -n "$DATABASE_NAME" ]; then
|
||||
verify_module_sql "$MODULE_NAME" "$DATABASE_NAME"
|
||||
else
|
||||
# Check all databases for this module
|
||||
for db in "$DB_AUTH_NAME" "$DB_WORLD_NAME" "$DB_CHARACTERS_NAME"; do
|
||||
if db_exists "$db"; then
|
||||
verify_module_sql "$MODULE_NAME" "$db"
|
||||
fi
|
||||
done
|
||||
if db_exists "$DB_PLAYERBOTS_NAME"; then
|
||||
verify_module_sql "$MODULE_NAME" "$DB_PLAYERBOTS_NAME"
|
||||
fi
|
||||
fi
|
||||
elif [ -n "$DATABASE_NAME" ]; then
|
||||
# List all updates in specific database
|
||||
list_module_updates "$DATABASE_NAME"
|
||||
else
|
||||
# Generate full report
|
||||
generate_verification_report
|
||||
fi
|
||||
|
||||
echo
|
||||
ok "Verification complete"
|
||||
echo
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user