Files
AzerothCore-RealmMaster/scripts/bash/manage-modules.sh
uprightbass360 5c9f1d7389 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
2025-11-20 18:26:00 -05:00

599 lines
18 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# Manifest-driven module management. Stages repositories, applies module
# metadata hooks, manages configuration files, and flags rebuild requirements.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
MODULE_HELPER="$PROJECT_ROOT/scripts/python/modules.py"
DEFAULT_ENV_PATH="$PROJECT_ROOT/.env"
ENV_PATH="${MODULES_ENV_PATH:-$DEFAULT_ENV_PATH}"
TEMPLATE_FILE="$PROJECT_ROOT/.env.template"
source "$PROJECT_ROOT/scripts/bash/project_name.sh"
# Default project name (read from .env or template)
DEFAULT_PROJECT_NAME="$(project_name::resolve "$ENV_PATH" "$TEMPLATE_FILE")"
BLUE='\033[0;34m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
PLAYERBOTS_DB_UPDATE_LOGGED=0
info(){ printf '%b\n' "${BLUE} $*${NC}"; }
ok(){ printf '%b\n' "${GREEN}$*${NC}"; }
warn(){ printf '%b\n' "${YELLOW}⚠️ $*${NC}"; }
err(){ printf '%b\n' "${RED}$*${NC}"; exit 1; }
# Declare module metadata arrays globally at script level
declare -A MODULE_NAME MODULE_REPO MODULE_REF MODULE_TYPE MODULE_ENABLED MODULE_NEEDS_BUILD MODULE_BLOCKED MODULE_POST_INSTALL MODULE_REQUIRES MODULE_CONFIG_CLEANUP MODULE_NOTES MODULE_STATUS MODULE_BLOCK_REASON
declare -a MODULE_KEYS
read_env_value(){
local key="$1" default="${2:-}" value="${!key:-}"
if [ -n "$value" ]; then
echo "$value"
return
fi
if [ -f "$ENV_PATH" ]; then
value="$(grep -E "^${key}=" "$ENV_PATH" 2>/dev/null | tail -n1 | cut -d'=' -f2- | tr -d '\r')"
value="$(echo "$value" | sed 's/[[:space:]]*#.*//' | sed 's/[[:space:]]*$//')"
if [[ "$value" == \"*\" && "$value" == *\" ]]; then
value="${value:1:-1}"
elif [[ "$value" == \'*\' && "$value" == *\' ]]; then
value="${value:1:-1}"
fi
fi
if [ -z "${value:-}" ]; then
value="$default"
fi
printf '%s\n' "${value}"
}
ensure_python(){
if ! command -v python3 >/dev/null 2>&1; then
err "python3 is required but not installed in PATH"
fi
}
resolve_manifest_path(){
if [ -n "${MODULES_MANIFEST_PATH:-}" ] && [ -f "${MODULES_MANIFEST_PATH}" ]; then
echo "${MODULES_MANIFEST_PATH}"
return
fi
local candidate
candidate="$PROJECT_ROOT/config/module-manifest.json"
if [ -f "$candidate" ]; then
echo "$candidate"
return
fi
candidate="/tmp/config/module-manifest.json"
if [ -f "$candidate" ]; then
echo "$candidate"
return
fi
err "Unable to locate module manifest (set MODULES_MANIFEST_PATH or ensure config/module-manifest.json exists)"
}
setup_git_config(){
info "Configuring git identity"
git config --global user.name "${GIT_USERNAME:-$DEFAULT_PROJECT_NAME}" >/dev/null 2>&1 || true
git config --global user.email "${GIT_EMAIL:-noreply@azerothcore.org}" >/dev/null 2>&1 || true
}
generate_module_state(){
mkdir -p "$STATE_DIR"
if ! python3 "$MODULE_HELPER" --env-path "$ENV_PATH" --manifest "$MANIFEST_PATH" generate --output-dir "$STATE_DIR"; then
err "Module manifest validation failed"
fi
local env_file="$STATE_DIR/modules.env"
if [ ! -f "$env_file" ]; then
err "modules.env not produced at $env_file"
fi
# shellcheck disable=SC1090
source "$env_file"
# Module arrays are already declared at script level
if ! MODULE_SHELL_STATE="$(python3 "$MODULE_HELPER" --env-path "$ENV_PATH" --manifest "$MANIFEST_PATH" dump --format shell)"; then
err "Unable to load manifest metadata"
fi
local eval_script
# Remove the declare line since we already declared the arrays
eval_script="$(echo "$MODULE_SHELL_STATE" | sed '/^declare -A /d')"
eval "$eval_script"
IFS=' ' read -r -a MODULES_COMPILE_LIST <<< "${MODULES_COMPILE:-}"
if [ "${#MODULES_COMPILE_LIST[@]}" -eq 1 ] && [ -z "${MODULES_COMPILE_LIST[0]}" ]; then
MODULES_COMPILE_LIST=()
fi
}
remove_disabled_modules(){
for key in "${MODULE_KEYS[@]}"; do
local dir
dir="${MODULE_NAME[$key]:-}"
[ -n "$dir" ] || continue
if [ "${MODULE_ENABLED[$key]:-0}" != "1" ] && [ -d "$dir" ]; then
info "Removing ${dir} (disabled)"
rm -rf "$dir"
fi
done
}
run_post_install_hooks(){
local key="$1"
local dir="$2"
local hooks_csv="${MODULE_POST_INSTALL[$key]:-}"
# Skip if no hooks defined
[ -n "$hooks_csv" ] || return 0
IFS=',' read -r -a hooks <<< "$hooks_csv"
local -a hook_search_paths=(
"$PROJECT_ROOT/scripts/hooks"
"/tmp/scripts/hooks"
"/scripts/hooks"
)
for hook in "${hooks[@]}"; do
[ -n "$hook" ] || continue
# Trim whitespace
hook="$(echo "$hook" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
local hook_script=""
local candidate
for candidate in "${hook_search_paths[@]}"; do
if [ -x "$candidate/$hook" ]; then
hook_script="$candidate/$hook"
break
fi
done
if [ -n "$hook_script" ]; then
info "Running post-install hook: $hook"
# Set hook environment variables
export MODULE_KEY="$key"
export MODULE_DIR="$dir"
export MODULE_NAME="${MODULE_NAME[$key]:-$(basename "$dir")}"
export MODULES_ROOT="${MODULES_ROOT:-/modules}"
export LUA_SCRIPTS_TARGET="/azerothcore/lua_scripts"
# Execute the hook script
if "$hook_script"; then
ok "Hook '$hook' completed successfully"
else
local exit_code=$?
case $exit_code in
1) warn "Hook '$hook' completed with warnings" ;;
*) err "Hook '$hook' failed with exit code $exit_code" ;;
esac
fi
# Clean up hook-specific environment (preserve MODULE_NAME array and script-level MODULES_ROOT)
unset MODULE_KEY MODULE_DIR LUA_SCRIPTS_TARGET
else
err "Hook script not found for ${hook} (searched: ${hook_search_paths[*]})"
fi
done
}
install_enabled_modules(){
for key in "${MODULE_KEYS[@]}"; do
if [ "${MODULE_ENABLED[$key]:-0}" != "1" ]; then
continue
fi
local dir repo ref
dir="${MODULE_NAME[$key]:-}"
repo="${MODULE_REPO[$key]:-}"
ref="${MODULE_REF[$key]:-}"
if [ -z "$dir" ] || [ -z "$repo" ]; then
warn "Missing repository metadata for $key"
continue
fi
if [ -d "$dir/.git" ]; then
info "$dir already present; skipping clone"
elif [ -d "$dir" ]; then
warn "$dir exists but is not a git repository; leaving in place"
else
info "Cloning ${dir} from ${repo}"
if ! git clone "$repo" "$dir"; then
err "Failed to clone $repo"
fi
if [ -n "$ref" ]; then
(cd "$dir" && git checkout "$ref") || warn "Unable to checkout ref $ref for $dir"
fi
fi
run_post_install_hooks "$key" "$dir"
done
}
update_playerbots_db_info(){
local target="$1"
if [ ! -f "$target" ] && [ ! -L "$target" ]; then
return 0
fi
local env_file="${ENV_PATH:-}"
local resolved
resolved="$(
python3 - "$target" "${env_file}" <<'PY'
import os
import pathlib
import sys
import re
def load_env_file(path):
data = {}
if not path:
return data
candidate = pathlib.Path(path)
if not candidate.is_file():
return data
for raw in candidate.read_text(encoding="utf-8", errors="ignore").splitlines():
if not raw or raw.lstrip().startswith("#"):
continue
if "=" not in raw:
continue
key, val = raw.split("=", 1)
key = key.strip()
val = val.strip()
if not key:
continue
if val and val[0] == val[-1] and val[0] in {"'", '"'}:
val = val[1:-1]
if "#" in val:
# Strip inline comments
val = val.split("#", 1)[0].rstrip()
data[key] = val
return data
def resolve_key(env_map, key, default=""):
value = os.environ.get(key)
if value:
return value
return env_map.get(key, default)
def parse_bool(value):
if value is None:
return None
value = value.strip().lower()
if value == "":
return None
if value in {"1", "true", "yes", "on"}:
return True
if value in {"0", "false", "no", "off"}:
return False
return None
def parse_int(value):
if value is None:
return None
value = value.strip()
if not value:
return None
if re.fullmatch(r"[+-]?\d+", value):
return str(int(value))
return None
def update_config(path_in, settings):
if not (os.path.exists(path_in) or os.path.islink(path_in)):
return False
path = os.path.realpath(path_in)
try:
with open(path, "r", encoding="utf-8", errors="ignore") as fh:
lines = fh.read().splitlines()
except FileNotFoundError:
lines = []
changed = False
pending = dict(settings)
for idx, raw in enumerate(lines):
stripped = raw.strip()
for key, value in list(pending.items()):
if re.match(rf"^\s*{re.escape(key)}\s*=", stripped):
desired = f"{key} = {value}"
if stripped != desired:
leading = raw[: len(raw) - len(raw.lstrip())]
trailing = ""
if "#" in raw:
before, comment = raw.split("#", 1)
if before.strip():
trailing = f" # {comment.strip()}"
lines[idx] = f"{leading}{desired}{trailing}"
changed = True
pending.pop(key, None)
break
if pending:
if lines and lines[-1] and not lines[-1].endswith("\n"):
lines[-1] = lines[-1] + "\n"
if lines and lines[-1].strip():
lines.append("\n")
for key, value in pending.items():
lines.append(f"{key} = {value}\n")
changed = True
if changed:
output = "\n".join(lines)
if output and not output.endswith("\n"):
output += "\n"
with open(path, "w", encoding="utf-8") as fh:
fh.write(output)
return True
target_path, env_path = sys.argv[1:3]
env_map = load_env_file(env_path)
host = resolve_key(env_map, "CONTAINER_MYSQL") or resolve_key(env_map, "MYSQL_HOST", "ac-mysql") or "ac-mysql"
port = resolve_key(env_map, "MYSQL_PORT", "3306") or "3306"
user = resolve_key(env_map, "MYSQL_USER", "root") or "root"
password = resolve_key(env_map, "MYSQL_ROOT_PASSWORD", "")
database = resolve_key(env_map, "DB_PLAYERBOTS_NAME", "acore_playerbots") or "acore_playerbots"
value = ";".join([host, port, user, password, database])
settings = {"PlayerbotsDatabaseInfo": f'"{value}"'}
enabled_setting = parse_bool(resolve_key(env_map, "PLAYERBOT_ENABLED"))
if enabled_setting is not None:
settings["AiPlayerbot.Enabled"] = "1" if enabled_setting else "0"
max_bots = parse_int(resolve_key(env_map, "PLAYERBOT_MAX_BOTS"))
min_bots = parse_int(resolve_key(env_map, "PLAYERBOT_MIN_BOTS"))
if max_bots and not min_bots:
min_bots = max_bots
if min_bots:
settings["AiPlayerbot.MinRandomBots"] = min_bots
if max_bots:
settings["AiPlayerbot.MaxRandomBots"] = max_bots
update_config(target_path, settings)
print(value)
PY
)" || return 0
local host port
host="${resolved%%;*}"
port="${resolved#*;}"
port="${port%%;*}"
if [ "$PLAYERBOTS_DB_UPDATE_LOGGED" = "0" ]; then
info "Updated PlayerbotsDatabaseInfo to use host ${host}:${port}"
PLAYERBOTS_DB_UPDATE_LOGGED=1
fi
return 0
}
manage_configuration_files(){
echo 'Managing configuration files...'
local env_target="${MODULES_ENV_TARGET_DIR:-}"
if [ -z "$env_target" ]; then
if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then
env_target="${MODULES_ROOT}/env/dist/etc"
else
env_target="/azerothcore/env/dist/etc"
fi
fi
mkdir -p "$env_target"
local key patterns_csv enabled pattern
for key in "${MODULE_KEYS[@]}"; do
enabled="${MODULE_ENABLED[$key]:-0}"
patterns_csv="${MODULE_CONFIG_CLEANUP[$key]:-}"
IFS=',' read -r -a patterns <<< "$patterns_csv"
if [ "${#patterns[@]}" -eq 1 ] && [ -z "${patterns[0]}" ]; then
unset patterns
continue
fi
for pattern in "${patterns[@]}"; do
[ -n "$pattern" ] || continue
if [ "$enabled" != "1" ]; then
rm -f "$env_target"/$pattern 2>/dev/null || true
fi
done
unset patterns
done
local modules_conf_dir="${env_target%/}/modules"
mkdir -p "$modules_conf_dir"
rm -rf "${modules_conf_dir}.backup"
rm -f "$modules_conf_dir"/*.conf "$modules_conf_dir"/*.conf.dist 2>/dev/null || true
local module_dir
for key in "${MODULE_KEYS[@]}"; do
module_dir="${MODULE_NAME[$key]:-}"
[ -n "$module_dir" ] || continue
[ -d "$module_dir" ] || continue
while IFS= read -r conf_file; do
[ -n "$conf_file" ] || continue
base_name="$(basename "$conf_file")"
# Ensure previous copies in root config are removed to keep modules/ canonical
main_conf_path="${env_target}/${base_name}"
if [ -f "$main_conf_path" ]; then
rm -f "$main_conf_path"
fi
if [[ "$base_name" == *.conf.dist ]]; then
root_conf="${env_target}/${base_name%.dist}"
if [ -f "$root_conf" ]; then
rm -f "$root_conf"
fi
fi
dest_path="${modules_conf_dir}/${base_name}"
cp "$conf_file" "$dest_path"
if [[ "$base_name" == *.conf.dist ]]; then
dest_conf="${modules_conf_dir}/${base_name%.dist}"
if [ ! -f "$dest_conf" ]; then
cp "$conf_file" "$dest_conf"
fi
fi
done < <(find "$module_dir" -path "*/conf/*" -type f \( -name "*.conf" -o -name "*.conf.dist" \) 2>/dev/null)
done
local playerbots_enabled="${MODULE_PLAYERBOTS:-0}"
if [ "${MODULE_ENABLED[MODULE_PLAYERBOTS]:-0}" = "1" ]; then
playerbots_enabled=1
fi
if [ "$playerbots_enabled" = "1" ]; then
update_playerbots_db_info "$modules_conf_dir/playerbots.conf"
update_playerbots_db_info "$modules_conf_dir/playerbots.conf.dist"
fi
if [ "${MODULE_AUTOBALANCE:-0}" = "1" ] && [ -f "$env_target/AutoBalance.conf.dist" ]; then
sed -i 's/^AutoBalance\.LevelScaling\.EndGameBoost.*/AutoBalance.LevelScaling.EndGameBoost = false # disabled pending proper implementation/' \
"$env_target/AutoBalance.conf.dist" || true
fi
}
load_sql_helper(){
local helper_paths=(
"/scripts/bash/manage-modules-sql.sh"
"/tmp/scripts/bash/manage-modules-sql.sh"
)
if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then
helper_paths+=("$PROJECT_ROOT/scripts/bash/manage-modules-sql.sh")
fi
local helper_path=""
for helper_path in "${helper_paths[@]}"; do
if [ -f "$helper_path" ]; then
# shellcheck disable=SC1090
. "$helper_path"
SQL_HELPER_PATH="$helper_path"
return 0
fi
done
err "SQL helper not found; expected manage-modules-sql.sh to be available"
}
# 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...'
local modules_state_file
if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then
modules_state_file="./.modules_state"
else
modules_state_file="/modules/.modules_state"
fi
local current_state=""
for key in "${MODULE_KEYS[@]}"; do
current_state+="${key}=${MODULE_ENABLED[$key]:-0}|"
done
local previous_state=""
if [ -f "$modules_state_file" ]; then
previous_state="$(cat "$modules_state_file")"
fi
local rebuild_required=0
if [ "$current_state" != "$previous_state" ]; then
if [ -n "$previous_state" ]; then
echo "🔄 Module configuration has changed - rebuild required"
else
echo "📝 First run - establishing module state baseline"
fi
rebuild_required=1
else
echo "✅ No module changes detected"
fi
echo "$current_state" > "$modules_state_file"
if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then
echo "🔧 Detected ${#MODULES_COMPILE_LIST[@]} enabled C++ modules requiring compilation:"
for mod in "${MODULES_COMPILE_LIST[@]}"; do
echo "$mod"
done
else
echo "✅ No C++ modules enabled - pre-built containers can be used"
fi
local rebuild_sentinel
if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then
if [ -n "${LOCAL_STORAGE_SENTINEL_PATH:-}" ]; then
rebuild_sentinel="${LOCAL_STORAGE_SENTINEL_PATH}"
else
rebuild_sentinel="./.requires_rebuild"
fi
else
rebuild_sentinel="/modules/.requires_rebuild"
fi
local host_rebuild_sentinel=""
if [ -n "${MODULES_HOST_DIR:-}" ]; then
host_rebuild_sentinel="${MODULES_HOST_DIR%/}/.requires_rebuild"
fi
if [ "$rebuild_required" = "1" ] && [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then
printf '%s\n' "${MODULES_COMPILE_LIST[@]}" > "$rebuild_sentinel"
if [ -n "$host_rebuild_sentinel" ]; then
printf '%s\n' "${MODULES_COMPILE_LIST[@]}" > "$host_rebuild_sentinel" 2>/dev/null || true
fi
echo "🚨 Module changes detected; run ./scripts/bash/rebuild-with-modules.sh to rebuild source images."
else
rm -f "$rebuild_sentinel" 2>/dev/null || true
if [ -n "$host_rebuild_sentinel" ]; then
rm -f "$host_rebuild_sentinel" 2>/dev/null || true
fi
fi
if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then
local target_dir="${MODULES_HOST_DIR:-$(pwd)}"
local desired_user
desired_user="$(id -u):$(id -g)"
if [ -d "$target_dir" ]; then
chown -R "$desired_user" "$target_dir" >/dev/null 2>&1 || true
chmod -R ug+rwX "$target_dir" >/dev/null 2>&1 || true
fi
fi
}
main(){
ensure_python
if [ "${MODULES_LOCAL_RUN:-0}" != "1" ]; then
cd /modules || err "Modules directory /modules not found"
fi
MODULES_ROOT="$(pwd)"
MANIFEST_PATH="$(resolve_manifest_path)"
STATE_DIR="${MODULES_HOST_DIR:-$MODULES_ROOT}"
setup_git_config
generate_module_state
remove_disabled_modules
install_enabled_modules
manage_configuration_files
# 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.
track_module_state
echo 'Module management complete.'
if [ "${MODULES_DEBUG_KEEPALIVE:-0}" = "1" ]; then
tail -f /dev/null
fi
}
main "$@"