feat: import native backup/<dbname>.conf+.sql pairs directly into alt-mariadb
This commit is contained in:
@@ -127,10 +127,188 @@ parse_native_db_conf() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mysql_import_sql_file() {
|
||||||
|
local database="$1" file="$2"
|
||||||
|
if [[ "$file" == *.gz ]]; then
|
||||||
|
gunzip -c "$file" | mysql_exec "$database"
|
||||||
|
return "${PIPESTATUS[1]}"
|
||||||
|
fi
|
||||||
|
mysql_exec "$database" < "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_database_exists() {
|
||||||
|
local db="$1"
|
||||||
|
[ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ] \
|
||||||
|
|| die "database was not restored: $db"
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuild_metadata_for_native_import() {
|
||||||
|
local db="$1" da_user="$2"
|
||||||
|
local i owner="" role host
|
||||||
|
for i in "${!NATIVE_DB_USER_NAMES[@]}"; do
|
||||||
|
role="${NATIVE_DB_USER_NAMES[$i]}"
|
||||||
|
host="${NATIVE_DB_USER_HOST[$i]:-localhost}"
|
||||||
|
[ -n "$owner" ] || owner="$role"
|
||||||
|
|
||||||
|
mysql_exec mysql -e "
|
||||||
|
INSERT INTO da_plugin_grant_profiles (db_name, role_name, privileges)
|
||||||
|
VALUES ('$(mysql_escape_literal "$db")', '$(mysql_escape_literal "$role")', '$(mysql_escape_literal "${NATIVE_DB_USER_PRIVS[$i]}")')
|
||||||
|
ON DUPLICATE KEY UPDATE privileges=VALUES(privileges), updated_at=CURRENT_TIMESTAMP;
|
||||||
|
" >> "$LOG_FILE" 2>&1 || true
|
||||||
|
|
||||||
|
mysql_exec mysql -e "
|
||||||
|
INSERT INTO da_plugin_access_hosts (da_user, role_name, host_pattern, note)
|
||||||
|
VALUES ('$(mysql_escape_literal "$da_user")', '$(mysql_escape_literal "$role")', '$(mysql_escape_literal "$host")', 'restored from native backup/<db>.conf')
|
||||||
|
ON DUPLICATE KEY UPDATE da_user=VALUES(da_user), note=VALUES(note), updated_at=CURRENT_TIMESTAMP;
|
||||||
|
" >> "$LOG_FILE" 2>&1 || true
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$owner" ]; then
|
||||||
|
mysql_exec mysql -e "
|
||||||
|
INSERT INTO da_plugin_database_owners (db_name, da_user, role_name)
|
||||||
|
VALUES ('$(mysql_escape_literal "$db")', '$(mysql_escape_literal "$da_user")', '$(mysql_escape_literal "$owner")')
|
||||||
|
ON DUPLICATE KEY UPDATE da_user=VALUES(da_user), role_name=VALUES(role_name), updated_at=CURRENT_TIMESTAMP;
|
||||||
|
" >> "$LOG_FILE" 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
import_native_database_from_conf() {
|
||||||
|
local db="$1" conf_file="$2" sql_file="$3" da_user="$4" overwrite="$5"
|
||||||
|
|
||||||
|
parse_native_db_conf "$conf_file" || { log "SKIP $db: conf did not parse"; return 1; }
|
||||||
|
|
||||||
|
local collation="${NATIVE_DB_COLLATION:-}"
|
||||||
|
if [ -z "$collation" ]; then
|
||||||
|
log "SKIP $db: no collation parsed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local i
|
||||||
|
for i in "${!NATIVE_DB_USER_PLUGIN[@]}"; do
|
||||||
|
if [ "${NATIVE_DB_USER_PLUGIN[$i]}" != "mysql_native_password" ]; then
|
||||||
|
log "SKIP $db: unsupported auth plugin '${NATIVE_DB_USER_PLUGIN[$i]}' for user ${NATIVE_DB_USER_NAMES[$i]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$overwrite" = "deny" ] && [ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ]; then
|
||||||
|
log "SKIP $db: already exists and overwrite is denied"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
backup_alt_database_for_rollback "$db" || { log "SKIP $db: could not create rollback dump"; return 1; }
|
||||||
|
|
||||||
|
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
|
||||||
|
log "SKIP $db: could not drop database before import"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET $(mysql_safe_identifier "$NATIVE_DB_CHARSET") COLLATE $(mysql_safe_identifier "$collation");" >> "$LOG_FILE" 2>&1; then
|
||||||
|
restore_alt_database_from_rollback "$db" || true
|
||||||
|
log "SKIP $db: could not create database before import"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local user host hash
|
||||||
|
for i in "${!NATIVE_DB_USER_NAMES[@]}"; do
|
||||||
|
user="${NATIVE_DB_USER_NAMES[$i]}"
|
||||||
|
host="${NATIVE_DB_USER_HOST[$i]:-localhost}"
|
||||||
|
hash="${NATIVE_DB_USER_HASH[$i]}"
|
||||||
|
|
||||||
|
if ! {
|
||||||
|
printf "DROP USER IF EXISTS '%s'@'%s';\n" "$(mysql_escape_literal "$user")" "$(mysql_escape_literal "$host")"
|
||||||
|
printf "CREATE USER '%s'@'%s' IDENTIFIED WITH mysql_native_password AS '%s';\n" \
|
||||||
|
"$(mysql_escape_literal "$user")" "$(mysql_escape_literal "$host")" "$(mysql_escape_literal "$hash")"
|
||||||
|
printf "GRANT %s ON %s.* TO '%s'@'%s';\n" \
|
||||||
|
"${NATIVE_DB_USER_PRIVS[$i]}" "$(mysql_quote_identifier "$db")" "$(mysql_escape_literal "$user")" "$(mysql_escape_literal "$host")"
|
||||||
|
} | mysql_exec mysql >> "$LOG_FILE" 2>&1; then
|
||||||
|
restore_alt_database_from_rollback "$db" || true
|
||||||
|
log "SKIP $db: could not recreate user ${user}@${host}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! mysql_import_sql_file "$db" "$sql_file" >> "$LOG_FILE" 2>&1; then
|
||||||
|
restore_alt_database_from_rollback "$db" || true
|
||||||
|
log "SKIP $db: could not import dump"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup_restore_rollback
|
||||||
|
verify_database_exists "$db"
|
||||||
|
rebuild_metadata_for_native_import "$db" "$da_user"
|
||||||
|
mysql_exec mysql -e "FLUSH PRIVILEGES;" >> "$LOG_FILE" 2>&1 || true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
find_native_pairs_in_dir() {
|
||||||
|
local root="$1" prefix="$2"
|
||||||
|
local conf base sql
|
||||||
|
while IFS= read -r conf; do
|
||||||
|
[ -n "$conf" ] || continue
|
||||||
|
base="$(basename "$conf" .conf)"
|
||||||
|
sql="$(dirname "$conf")/${base}.sql"
|
||||||
|
[ -r "$sql" ] || continue
|
||||||
|
printf '%s\t%s\t%s\n' "$base" "$conf" "$sql"
|
||||||
|
done < <(find "$root" -maxdepth 4 -type f -name "${prefix}_*.conf" 2>/dev/null | sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local da_user="" root="" overwrite="allow"
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--user) da_user="${2:-}"; shift 2 ;;
|
||||||
|
--root) root="${2:-}"; shift 2 ;;
|
||||||
|
--overwrite) overwrite="${2:-}"; shift 2 ;;
|
||||||
|
*) die "unknown argument: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$da_user" ] || die "--user is required"
|
||||||
|
[ -n "$root" ] || die "--root is required"
|
||||||
|
[[ "$da_user" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]] || die "unsafe DirectAdmin username: $da_user"
|
||||||
|
[ -d "$root" ] || die "backup root directory does not exist: $root"
|
||||||
|
|
||||||
|
mysql_load_alt_credentials
|
||||||
|
mysql_ensure_metadata_schema
|
||||||
|
|
||||||
|
local -a handled=()
|
||||||
|
local base conf sql
|
||||||
|
while IFS=$'\t' read -r base conf sql; do
|
||||||
|
[ -n "$base" ] || continue
|
||||||
|
handled+=("$base")
|
||||||
|
if [[ "$base" != "${da_user}_"* ]]; then
|
||||||
|
printf 'SKIPPED %s wrong-user-prefix\n' "$base"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if import_native_database_from_conf "$base" "$conf" "$sql" "$da_user" "$overwrite"; then
|
||||||
|
printf 'IMPORTED %s\n' "$base"
|
||||||
|
else
|
||||||
|
printf 'SKIPPED %s import-failed\n' "$base"
|
||||||
|
fi
|
||||||
|
done < <(find_native_pairs_in_dir "$root" "$da_user")
|
||||||
|
|
||||||
|
# Any *.sql belonging to this user's prefix with no matching .conf was not
|
||||||
|
# covered by the loop above (find_native_pairs_in_dir only yields pairs
|
||||||
|
# that have both files) - report those explicitly so the caller knows to
|
||||||
|
# fall back to native-staging-migrate for them too.
|
||||||
|
local sql_base
|
||||||
|
while IFS= read -r sql; do
|
||||||
|
[ -n "$sql" ] || continue
|
||||||
|
sql_base="$(basename "$sql" .sql)"
|
||||||
|
[[ "$sql_base" == "${da_user}_"* ]] || continue
|
||||||
|
local already=0 h
|
||||||
|
for h in "${handled[@]}"; do
|
||||||
|
[ "$h" = "$sql_base" ] && { already=1; break; }
|
||||||
|
done
|
||||||
|
[ "$already" -eq 1 ] || printf 'SKIPPED %s no-matching-conf\n' "$sql_base"
|
||||||
|
done < <(find "$root" -maxdepth 4 -type f -name "${da_user}_*.sql" 2>/dev/null | sort)
|
||||||
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
[ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE"
|
[ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE"
|
||||||
# shellcheck source=../setup/mysql_common.sh
|
# shellcheck source=../setup/mysql_common.sh
|
||||||
source "$COMMON_FILE"
|
source "$COMMON_FILE"
|
||||||
mysql_load_plugin_settings_file "$SETTINGS_FILE"
|
mysql_load_plugin_settings_file "$SETTINGS_FILE"
|
||||||
# main() is added in Task 3.
|
main "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
SCRIPT="$PLUGIN_DIR/scripts/da-integration/alt_mysql_native_backup_restore.sh"
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "FAIL: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Fake alt-mariadb client, tracks statements + a simple existing-db state file ---
|
||||||
|
mkdir -p "$TMP_DIR/mariadb/bin"
|
||||||
|
cat > "$TMP_DIR/mariadb/bin/mariadb" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
STATE_FILE="${FAKE_DB_STATE_FILE:?FAKE_DB_STATE_FILE must be set}"
|
||||||
|
CALL_LOG="${FAKE_CALL_LOG:?FAKE_CALL_LOG must be set}"
|
||||||
|
|
||||||
|
sql=""
|
||||||
|
mode=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
-e) mode="sql" ;;
|
||||||
|
-Nse) mode="sql" ;;
|
||||||
|
--database=*|--user=*|--socket=*|--host=*|--port=*|--protocol=*) ;;
|
||||||
|
*)
|
||||||
|
if [ "$mode" = "sql" ]; then
|
||||||
|
sql="$arg"
|
||||||
|
mode=""
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$sql" ]; then
|
||||||
|
printf '%s\n' "$sql" >> "$CALL_LOG"
|
||||||
|
case "$sql" in
|
||||||
|
*"SELECT 1 FROM information_schema.SCHEMATA"*)
|
||||||
|
name="$(printf '%s' "$sql" | sed -n "s/.*SCHEMA_NAME='\([^']*\)'.*/\1/p")"
|
||||||
|
grep -qx "$name" "$STATE_FILE" 2>/dev/null && echo 1
|
||||||
|
;;
|
||||||
|
*"DROP DATABASE"*)
|
||||||
|
name="$(printf '%s' "$sql" | sed -n 's/.*DATABASE[^`]*`\([A-Za-z0-9_]*\)`.*/\1/p')"
|
||||||
|
[ -n "$name" ] && { grep -vx "$name" "$STATE_FILE" > "$STATE_FILE.tmp" 2>/dev/null || true; mv -f "$STATE_FILE.tmp" "$STATE_FILE"; }
|
||||||
|
;;
|
||||||
|
*"CREATE DATABASE"*)
|
||||||
|
name="$(printf '%s' "$sql" | sed -n 's/.*DATABASE[^`]*`\([A-Za-z0-9_]*\)`.*/\1/p')"
|
||||||
|
[ -n "$name" ] && echo "$name" >> "$STATE_FILE"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >> "$CALL_LOG"
|
||||||
|
printf 'IMPORT\n' >> "$CALL_LOG"
|
||||||
|
if [ "${FAKE_IMPORT_FAIL:-0}" = "1" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
STUB
|
||||||
|
chmod 755 "$TMP_DIR/mariadb/bin/mariadb"
|
||||||
|
|
||||||
|
cat > "$TMP_DIR/mariadb/bin/mariadb-dump" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
echo "-- fake rollback dump"
|
||||||
|
exit 0
|
||||||
|
STUB
|
||||||
|
chmod 755 "$TMP_DIR/mariadb/bin/mariadb-dump"
|
||||||
|
|
||||||
|
cat > "$TMP_DIR/alt-mysql.conf" <<EOF
|
||||||
|
host=
|
||||||
|
passwd=secret
|
||||||
|
port=4407
|
||||||
|
socket=$TMP_DIR/data/mariadb.sock
|
||||||
|
user=da_admin
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$TMP_DIR/settings.conf" <<EOF
|
||||||
|
MYSQL_PLUGIN_CREDENTIALS_FILE=$TMP_DIR/alt-mysql.conf
|
||||||
|
MYSQL_PLUGIN_CLIENT_BIN_DIR=$TMP_DIR/mariadb/bin
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# run_import NAME_PREFIX -- ARGS...
|
||||||
|
# Runs the script under test with fresh, isolated state/log/call-log files
|
||||||
|
# for this scenario, writes its stdout to "$TMP_DIR/<prefix>.stdout" and
|
||||||
|
# writes every mariadb-stub SQL statement to "$TMP_DIR/<prefix>.calls",
|
||||||
|
# then prints nothing (the two files are read directly by the caller).
|
||||||
|
run_import() {
|
||||||
|
local prefix="$1"
|
||||||
|
shift
|
||||||
|
local db_state="$TMP_DIR/${prefix}.dbstate"
|
||||||
|
local call_log="$TMP_DIR/${prefix}.calls"
|
||||||
|
local stdout_file="$TMP_DIR/${prefix}.stdout"
|
||||||
|
local restore_log="$TMP_DIR/${prefix}.restore.log"
|
||||||
|
: > "$db_state"
|
||||||
|
: > "$call_log"
|
||||||
|
|
||||||
|
DA_ALT_MYSQL_SETTINGS_FILE="$TMP_DIR/settings.conf" \
|
||||||
|
DA_ALT_MYSQL_RESTORE_LOG="$restore_log" \
|
||||||
|
FAKE_DB_STATE_FILE="$db_state" \
|
||||||
|
FAKE_CALL_LOG="$call_log" \
|
||||||
|
bash "$SCRIPT" "$@" > "$stdout_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Build a fake backup directory with one valid database pair ---
|
||||||
|
BACKUP_DIR="$TMP_DIR/backup"
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
cat > "$BACKUP_DIR/demo_app.conf" <<'EOF'
|
||||||
|
accesshosts=0=localhost
|
||||||
|
db_collation=DEFAULT_CHARACTER_SET_NAME=utf8mb4&DEFAULT_COLLATION_NAME=utf8mb4_unicode_ci&SCHEMA_NAME=demo_app
|
||||||
|
demo_app=accesshosts=localhost&select_priv=Y&insert_priv=Y&passwd=%2Aabc123&plugin=mysql_native_password
|
||||||
|
EOF
|
||||||
|
cat > "$BACKUP_DIR/demo_app.sql" <<'EOF'
|
||||||
|
CREATE TABLE t (id INT);
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# --- Scenario 1: happy path, single valid database ---
|
||||||
|
run_import scenario1 --user demo --root "$BACKUP_DIR" --overwrite allow
|
||||||
|
|
||||||
|
grep -q "IMPORTED demo_app" "$TMP_DIR/scenario1.stdout" \
|
||||||
|
|| fail "expected 'IMPORTED demo_app' in output, got: $(cat "$TMP_DIR/scenario1.stdout")"
|
||||||
|
grep -q "CREATE USER 'demo_app'@'localhost'" "$TMP_DIR/scenario1.calls" \
|
||||||
|
|| fail "expected CREATE USER statement for demo_app@localhost"
|
||||||
|
grep -q "GRANT SELECT,INSERT ON \`demo_app\`.\* TO 'demo_app'@'localhost'" "$TMP_DIR/scenario1.calls" \
|
||||||
|
|| fail "expected GRANT SELECT,INSERT for demo_app@localhost"
|
||||||
|
grep -q "IMPORT" "$TMP_DIR/scenario1.calls" || fail "expected the .sql dump to be piped in for import"
|
||||||
|
|
||||||
|
# --- Scenario 2: a database with no matching .conf must be skipped, not abort the run ---
|
||||||
|
mkdir -p "$TMP_DIR/backup_missing_conf"
|
||||||
|
cat > "$TMP_DIR/backup_missing_conf/orphan_db.sql" <<'EOF'
|
||||||
|
CREATE TABLE t (id INT);
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run_import scenario2 --user orphan --root "$TMP_DIR/backup_missing_conf" --overwrite allow
|
||||||
|
|
||||||
|
grep -q "SKIPPED orphan_db" "$TMP_DIR/scenario2.stdout" \
|
||||||
|
|| fail "expected orphan_db (no .conf) to be reported as SKIPPED, got: $(cat "$TMP_DIR/scenario2.stdout")"
|
||||||
|
|
||||||
|
echo "alt_mysql_native_backup_restore_import_test: OK"
|
||||||
Reference in New Issue
Block a user