f472a7abfc
alt_mysql_restore_payload.sh (the DirectAdmin native-restore hook that imports a plugin-origin backup payload into alt-mariadb) had zero behavioral test coverage - only hook-installation plumbing was tested. Add a fake mariadb/mariadb-dump client stub (tracking a simple existing-databases state file) to exercise the real script end-to-end without a live server, covering: checksum mismatch rejection, overwrite=deny enforcement against an existing database, and the happy-path restore. Add a DA_ALT_MYSQL_SETTINGS_FILE override (same pattern as worker.sh's PLUGIN_DIR) so the script's settings file can be pointed at a test fixture instead of the real system one. Also extend da_integration_install_test.sh to check hook-symlink install/uninstall coverage for database_delete_pre.sh, database_user_password_change_pre.sh, database_user_create_post.sh, and database_destroy_user_post.sh, which were previously untested (only database_create_pre.sh was checked).
294 lines
9.6 KiB
Bash
294 lines
9.6 KiB
Bash
#!/bin/bash
|
|
set -Eeuo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
COMMON_FILE="$PLUGIN_DIR/scripts/setup/mysql_common.sh"
|
|
SETTINGS_FILE="${DA_ALT_MYSQL_SETTINGS_FILE:-$PLUGIN_DIR/plugin-settings.conf}"
|
|
LOG_FILE="${DA_ALT_MYSQL_RESTORE_LOG:-$PLUGIN_DIR/data/logs/da-restore.log}"
|
|
DA_USER=""
|
|
PAYLOAD_DIR=""
|
|
OVERWRITE_POLICY=""
|
|
RESTORE_ROLLBACK_FILE=""
|
|
RESTORE_ROLLBACK_CREATED=0
|
|
|
|
mkdir -p "$(dirname "$LOG_FILE")"
|
|
|
|
log() {
|
|
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
|
|
}
|
|
|
|
die() {
|
|
log "ERROR: $*"
|
|
printf 'alt-mysql restore payload: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
usage() {
|
|
cat >&2 <<'EOF'
|
|
Usage: alt_mysql_restore_payload.sh --user DA_USER --payload /path/to/backup/alt_mysql [--overwrite allow|deny]
|
|
EOF
|
|
exit 2
|
|
}
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--user) DA_USER="${2:-}"; shift 2 ;;
|
|
--payload) PAYLOAD_DIR="${2:-}"; shift 2 ;;
|
|
--overwrite) OVERWRITE_POLICY="${2:-}"; shift 2 ;;
|
|
*) usage ;;
|
|
esac
|
|
done
|
|
|
|
[ -n "$DA_USER" ] || usage
|
|
[ -n "$PAYLOAD_DIR" ] || usage
|
|
[[ "$DA_USER" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]] || die "unsafe DirectAdmin username: $DA_USER"
|
|
[ -d "$PAYLOAD_DIR" ] || die "payload directory does not exist: $PAYLOAD_DIR"
|
|
[ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE"
|
|
# shellcheck source=../setup/mysql_common.sh
|
|
source "$COMMON_FILE"
|
|
mysql_load_plugin_settings_file "$SETTINGS_FILE"
|
|
|
|
read_plugin_setting() {
|
|
local key="$1"
|
|
local default="$2"
|
|
local line value
|
|
[ -r "$SETTINGS_FILE" ] || {
|
|
printf '%s\n' "$default"
|
|
return 0
|
|
}
|
|
line="$(grep -E "^${key}=" "$SETTINGS_FILE" | tail -n 1 || true)"
|
|
[ -n "$line" ] || {
|
|
printf '%s\n' "$default"
|
|
return 0
|
|
}
|
|
value="${line#*=}"
|
|
value="${value%%#*}"
|
|
value="${value%\"}"
|
|
value="${value#\"}"
|
|
value="${value%\'}"
|
|
value="${value#\'}"
|
|
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
printf '%s\n' "${value:-$default}"
|
|
}
|
|
|
|
sha256_file() {
|
|
local file="$1"
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum "$file" | awk '{print $1}'
|
|
return 0
|
|
fi
|
|
if command -v shasum >/dev/null 2>&1; then
|
|
shasum -a 256 "$file" | awk '{print $1}'
|
|
return 0
|
|
fi
|
|
die "missing sha256sum/shasum"
|
|
}
|
|
|
|
require_php_cli() {
|
|
command -v php >/dev/null 2>&1 || die "php CLI is required to parse manifest.json"
|
|
}
|
|
|
|
manifest_database_rows() {
|
|
local manifest="$1"
|
|
php -r '
|
|
$manifest = $argv[1];
|
|
$data = json_decode(file_get_contents($manifest), true);
|
|
if (!is_array($data) || ($data["format"] ?? "") !== "da-alt-mysql-v1") {
|
|
fwrite(STDERR, "invalid manifest\n");
|
|
exit(3);
|
|
}
|
|
foreach (($data["databases"] ?? []) as $db) {
|
|
$name = (string)($db["name"] ?? "");
|
|
$file = (string)($db["file"] ?? "");
|
|
$sha = (string)($db["sha256"] ?? "");
|
|
if ($name === "" || $file === "" || $sha === "") {
|
|
fwrite(STDERR, "invalid database entry\n");
|
|
exit(4);
|
|
}
|
|
echo $name, "\t", $file, "\t", $sha, "\n";
|
|
}
|
|
' "$manifest"
|
|
}
|
|
|
|
safe_database_name() {
|
|
local db="$1"
|
|
[[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || die "unsafe database name in payload: $db"
|
|
printf '%s\n' "$db"
|
|
}
|
|
|
|
database_allowed_for_user() {
|
|
local db="$1"
|
|
[[ "$db" == "${DA_USER}_"* ]]
|
|
}
|
|
|
|
import_gzip_sql() {
|
|
local database="$1"
|
|
local file="$2"
|
|
if [[ "$file" == *.gz ]]; then
|
|
gunzip -c "$file" | mysql_exec "$database"
|
|
return "${PIPESTATUS[1]}"
|
|
fi
|
|
mysql_exec "$database" < "$file"
|
|
}
|
|
|
|
cleanup_restore_rollback() {
|
|
if [ -n "${RESTORE_ROLLBACK_FILE:-}" ] && [ -f "$RESTORE_ROLLBACK_FILE" ]; then
|
|
rm -f "$RESTORE_ROLLBACK_FILE"
|
|
fi
|
|
RESTORE_ROLLBACK_FILE=""
|
|
RESTORE_ROLLBACK_CREATED=0
|
|
}
|
|
|
|
backup_alt_database_for_rollback() {
|
|
local db="$1"
|
|
cleanup_restore_rollback
|
|
RESTORE_ROLLBACK_FILE="$(mktemp)"
|
|
|
|
if [ -z "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1" 2>>"$LOG_FILE" || true)" ]; then
|
|
cleanup_restore_rollback
|
|
return 0
|
|
fi
|
|
|
|
log "creating rollback dump: $db"
|
|
if mysqldump_exec \
|
|
--single-transaction \
|
|
--quick \
|
|
--skip-lock-tables \
|
|
--routines \
|
|
--triggers \
|
|
--events \
|
|
--default-character-set=utf8mb4 \
|
|
"$db" > "$RESTORE_ROLLBACK_FILE" 2>>"$LOG_FILE"; then
|
|
RESTORE_ROLLBACK_CREATED=1
|
|
return 0
|
|
fi
|
|
|
|
cleanup_restore_rollback
|
|
return 1
|
|
}
|
|
|
|
restore_alt_database_from_rollback() {
|
|
local db="$1"
|
|
if [ "$RESTORE_ROLLBACK_CREATED" -ne 1 ] || [ -z "${RESTORE_ROLLBACK_FILE:-}" ] || [ ! -r "$RESTORE_ROLLBACK_FILE" ]; then
|
|
return 0
|
|
fi
|
|
|
|
log "restoring rollback dump: $db"
|
|
mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db");" >> "$LOG_FILE" 2>&1 || true
|
|
mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG_FILE" 2>&1 || true
|
|
mysql_exec "$db" < "$RESTORE_ROLLBACK_FILE" >> "$LOG_FILE" 2>&1 || {
|
|
log "ERROR: rollback restore failed for $db"
|
|
return 1
|
|
}
|
|
}
|
|
|
|
trap cleanup_restore_rollback INT TERM EXIT
|
|
|
|
metadata_cleanup_for_user() {
|
|
local escaped_user
|
|
escaped_user="$(mysql_escape_literal "$DA_USER")"
|
|
mysql_exec mysql -e "DELETE FROM da_plugin_access_hosts WHERE da_user='${escaped_user}' OR role_name LIKE '${escaped_user}\\_%';"
|
|
mysql_exec mysql -e "DELETE FROM da_plugin_grant_profiles WHERE db_name LIKE '${escaped_user}\\_%' OR role_name LIKE '${escaped_user}\\_%';"
|
|
mysql_exec mysql -e "DELETE FROM da_plugin_database_owners WHERE db_name LIKE '${escaped_user}\\_%' OR role_name LIKE '${escaped_user}\\_%';"
|
|
}
|
|
|
|
verify_database_exists() {
|
|
local db="$1"
|
|
local escaped_db
|
|
escaped_db="$(mysql_escape_literal "$db")"
|
|
[ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='${escaped_db}' LIMIT 1")" ] \
|
|
|| die "database was not restored: $db"
|
|
}
|
|
|
|
sync_remote_hosts_if_enabled() {
|
|
local allow sync_script
|
|
allow="$(read_plugin_setting allow_remote_hosts 0)"
|
|
[ "$allow" = "1" ] || return 0
|
|
sync_script="$(read_plugin_setting MYSQL_PLUGIN_SUDO_SYNC_HOSTS /usr/local/directadmin/plugins/alt-mysql/scripts/setup/mysql_host_sync.sh)"
|
|
if [ -x "$sync_script" ]; then
|
|
"$sync_script" >> "$LOG_FILE" 2>&1 || log "WARNING: host sync failed after restore"
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
local manifest users_file grants_file metadata_file db file rel_file expected_sha actual_sha overwrite
|
|
manifest="$PAYLOAD_DIR/manifest.json"
|
|
users_file="$PAYLOAD_DIR/grants/users.sql.gz"
|
|
grants_file="$PAYLOAD_DIR/grants/grants.sql.gz"
|
|
[ -r "$manifest" ] || die "missing manifest: $manifest"
|
|
[ -r "$users_file" ] || die "missing users payload: $users_file"
|
|
[ -r "$grants_file" ] || die "missing grants payload: $grants_file"
|
|
|
|
require_php_cli
|
|
mysql_load_alt_credentials
|
|
mysql_ensure_metadata_schema
|
|
|
|
overwrite="${OVERWRITE_POLICY:-$(read_plugin_setting DA_ALT_MYSQL_RESTORE_OVERWRITE allow)}"
|
|
case "$overwrite" in
|
|
allow|deny) ;;
|
|
prompt) overwrite="deny" ;;
|
|
*) overwrite="deny" ;;
|
|
esac
|
|
|
|
log "restore payload start: user=$DA_USER payload=$PAYLOAD_DIR overwrite=$overwrite"
|
|
manifest_database_rows "$manifest" >/dev/null
|
|
|
|
while IFS=$'\t' read -r db rel_file expected_sha; do
|
|
db="$(safe_database_name "$db")"
|
|
database_allowed_for_user "$db" || die "database $db does not match target user prefix $DA_USER"
|
|
file="$PAYLOAD_DIR/$rel_file"
|
|
[ -r "$file" ] || die "missing database dump: $file"
|
|
actual_sha="$(sha256_file "$file")"
|
|
[ "$actual_sha" = "$expected_sha" ] || die "checksum mismatch for $db"
|
|
if [ "$overwrite" = "deny" ] && [ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ]; then
|
|
die "target database exists and overwrite is denied: $db"
|
|
fi
|
|
done < <(manifest_database_rows "$manifest")
|
|
|
|
log "restoring user definitions"
|
|
import_gzip_sql mysql "$users_file" >> "$LOG_FILE" 2>&1 || die "cannot restore MySQL users from payload"
|
|
|
|
metadata_cleanup_for_user
|
|
|
|
while IFS=$'\t' read -r db rel_file expected_sha; do
|
|
db="$(safe_database_name "$db")"
|
|
file="$PAYLOAD_DIR/$rel_file"
|
|
log "restoring database: $db"
|
|
backup_alt_database_for_rollback "$db" || die "cannot create rollback dump before restoring database: $db"
|
|
if ! mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db");" >> "$LOG_FILE" 2>&1; then
|
|
restore_alt_database_from_rollback "$db" || true
|
|
die "cannot drop database before restore: $db"
|
|
fi
|
|
if ! mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG_FILE" 2>&1; then
|
|
restore_alt_database_from_rollback "$db" || true
|
|
die "cannot create database before restore: $db"
|
|
fi
|
|
if ! import_gzip_sql "$db" "$file" >> "$LOG_FILE" 2>&1; then
|
|
restore_alt_database_from_rollback "$db" || true
|
|
die "cannot import database dump: $db"
|
|
fi
|
|
cleanup_restore_rollback
|
|
verify_database_exists "$db"
|
|
done < <(manifest_database_rows "$manifest")
|
|
|
|
for metadata_file in \
|
|
"$PAYLOAD_DIR/metadata/da_plugin_database_owners.sql.gz" \
|
|
"$PAYLOAD_DIR/metadata/da_plugin_grant_profiles.sql.gz" \
|
|
"$PAYLOAD_DIR/metadata/da_plugin_access_hosts.sql.gz"; do
|
|
if [ -r "$metadata_file" ]; then
|
|
log "restoring metadata: ${metadata_file##*/}"
|
|
import_gzip_sql mysql "$metadata_file" >> "$LOG_FILE" 2>&1 || die "cannot restore metadata: $metadata_file"
|
|
fi
|
|
done
|
|
|
|
log "restoring grants"
|
|
import_gzip_sql mysql "$grants_file" >> "$LOG_FILE" 2>&1 || die "cannot restore grants from payload"
|
|
mysql_exec mysql -e "FLUSH PRIVILEGES;" >> "$LOG_FILE" 2>&1 || true
|
|
sync_remote_hosts_if_enabled
|
|
|
|
log "restore payload completed: user=$DA_USER"
|
|
}
|
|
|
|
main "$@"
|