Import alt-mysql plugin
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
#!/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="$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 "$@"
|
||||
@@ -0,0 +1,312 @@
|
||||
#!/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"
|
||||
LOG_FILE="${DA_ALT_MYSQL_BACKUP_LOG:-$PLUGIN_DIR/data/logs/da-backup.log}"
|
||||
DRY_RUN=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
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 backup hook: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE"
|
||||
# shellcheck source=../setup/mysql_common.sh
|
||||
source "$COMMON_FILE"
|
||||
mysql_load_plugin_settings_file "$PLUGIN_DIR/plugin-settings.conf"
|
||||
|
||||
json_escape() {
|
||||
printf '%s' "${1:-}" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
json_string() {
|
||||
printf '"%s"' "$(json_escape "${1:-}")"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
detect_da_user() {
|
||||
local value="${DA_USER:-${username:-${user:-${USERNAME:-}}}}"
|
||||
if [ -z "$value" ] && [ "${1:-}" != "--dry-run" ]; then
|
||||
value="${1:-}"
|
||||
fi
|
||||
if [[ ! "$value" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]]; then
|
||||
die "cannot detect safe DirectAdmin username"
|
||||
fi
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
candidate_dir_if_valid() {
|
||||
local candidate="${1:-}"
|
||||
[ -n "$candidate" ] || return 1
|
||||
[ -d "$candidate" ] || return 1
|
||||
printf '%s\n' "$(cd "$candidate" && pwd -P)"
|
||||
}
|
||||
|
||||
candidate_backup_root_if_valid() {
|
||||
local candidate="${1:-}"
|
||||
local dir=""
|
||||
if ! dir="$(candidate_dir_if_valid "$candidate")"; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$DRY_RUN" -eq 1 ] || [ -d "$dir/backup" ]; then
|
||||
printf '%s\n' "$dir"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
detect_backup_root() {
|
||||
local current candidate dir file_candidate
|
||||
current="$(pwd -P)"
|
||||
|
||||
for candidate in \
|
||||
"${DA_ALT_MYSQL_BACKUP_ROOT:-}" \
|
||||
"${backup_path:-}" \
|
||||
"${backupdir:-}" \
|
||||
"${backup_dir:-}" \
|
||||
"${tmpdir:-}" \
|
||||
"$current"; do
|
||||
if dir="$(candidate_backup_root_if_valid "$candidate")"; then
|
||||
printf '%s\n' "$dir"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
for file_candidate in "${file:-}" "${filename:-}" "${backup_file:-}"; do
|
||||
[ -n "$file_candidate" ] || continue
|
||||
candidate="$(dirname "$file_candidate")"
|
||||
if dir="$(candidate_backup_root_if_valid "$candidate")"; then
|
||||
printf '%s\n' "$dir"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
die "cannot detect DirectAdmin backup assembly directory"
|
||||
}
|
||||
|
||||
list_user_databases() {
|
||||
local da_user="$1"
|
||||
local escaped_user
|
||||
escaped_user="$(mysql_escape_literal "$da_user")"
|
||||
mysql_exec mysql -Nse "
|
||||
SELECT SCHEMA_NAME
|
||||
FROM information_schema.SCHEMATA
|
||||
WHERE SCHEMA_NAME LIKE '${escaped_user}\\_%' ESCAPE '\\'
|
||||
ORDER BY SCHEMA_NAME
|
||||
" | grep -Ev '^(information_schema|performance_schema|mysql|sys)$' || true
|
||||
}
|
||||
|
||||
list_user_roles() {
|
||||
local da_user="$1"
|
||||
local escaped_user
|
||||
escaped_user="$(mysql_escape_literal "$da_user")"
|
||||
mysql_exec mysql -Nse "
|
||||
SELECT DISTINCT User
|
||||
FROM mysql.user
|
||||
WHERE User LIKE '${escaped_user}\\_%' ESCAPE '\\'
|
||||
AND User <> ''
|
||||
ORDER BY User
|
||||
" || true
|
||||
}
|
||||
|
||||
dump_users_file() {
|
||||
local da_user="$1"
|
||||
local output_file="$2"
|
||||
local tmp_file user host q_user q_host create_sql
|
||||
|
||||
tmp_file="$(mktemp)"
|
||||
{
|
||||
printf '%s\n' "-- alt-mysql user definitions for $da_user"
|
||||
printf '%s\n' "SET sql_log_bin=0;"
|
||||
} > "$tmp_file"
|
||||
|
||||
while IFS=$'\t' read -r user host; do
|
||||
[ -n "${user:-}" ] || continue
|
||||
[ -n "${host:-}" ] || continue
|
||||
q_user="$(mysql_escape_literal "$user")"
|
||||
q_host="$(mysql_escape_literal "$host")"
|
||||
create_sql="$(mysql_exec mysql -Nse "SHOW CREATE USER '${q_user}'@'${q_host}'" | awk -F'\t' '{print $NF}' || true)"
|
||||
[ -n "$create_sql" ] || continue
|
||||
printf "DROP USER IF EXISTS '%s'@'%s';\n" "$q_user" "$q_host" >> "$tmp_file"
|
||||
printf '%s;\n' "$create_sql" >> "$tmp_file"
|
||||
done < <(
|
||||
mysql_exec mysql -Nse "
|
||||
SELECT User, Host
|
||||
FROM mysql.user
|
||||
WHERE User LIKE '$(mysql_escape_literal "$da_user")\\_%' ESCAPE '\\'
|
||||
AND User <> ''
|
||||
ORDER BY User, Host
|
||||
" || true
|
||||
)
|
||||
|
||||
printf '%s\n' "SET sql_log_bin=1;" >> "$tmp_file"
|
||||
gzip -9 < "$tmp_file" > "$output_file"
|
||||
rm -f "$tmp_file"
|
||||
}
|
||||
|
||||
dump_grants_file() {
|
||||
local da_user="$1"
|
||||
local output_file="$2"
|
||||
local tmp_file user host q_user q_host grant_line
|
||||
|
||||
tmp_file="$(mktemp)"
|
||||
printf '%s\n' "-- alt-mysql grants for $da_user" > "$tmp_file"
|
||||
|
||||
while IFS=$'\t' read -r user host; do
|
||||
[ -n "${user:-}" ] || continue
|
||||
[ -n "${host:-}" ] || continue
|
||||
q_user="$(mysql_escape_literal "$user")"
|
||||
q_host="$(mysql_escape_literal "$host")"
|
||||
while IFS= read -r grant_line; do
|
||||
[ -n "$grant_line" ] || continue
|
||||
printf '%s;\n' "$grant_line" >> "$tmp_file"
|
||||
done < <(mysql_exec mysql -Nse "SHOW GRANTS FOR '${q_user}'@'${q_host}'" || true)
|
||||
printf '\n' >> "$tmp_file"
|
||||
done < <(
|
||||
mysql_exec mysql -Nse "
|
||||
SELECT User, Host
|
||||
FROM mysql.user
|
||||
WHERE User LIKE '$(mysql_escape_literal "$da_user")\\_%' ESCAPE '\\'
|
||||
AND User <> ''
|
||||
ORDER BY User, Host
|
||||
" || true
|
||||
)
|
||||
|
||||
gzip -9 < "$tmp_file" > "$output_file"
|
||||
rm -f "$tmp_file"
|
||||
}
|
||||
|
||||
dump_metadata_table() {
|
||||
local table="$1"
|
||||
local where="$2"
|
||||
local output_file="$3"
|
||||
mysqldump_exec \
|
||||
--single-transaction \
|
||||
--skip-lock-tables \
|
||||
--no-create-info \
|
||||
--compact \
|
||||
--where="$where" \
|
||||
mysql "$table" | gzip -9 > "$output_file"
|
||||
}
|
||||
|
||||
write_manifest() {
|
||||
local da_user="$1"
|
||||
local payload_dir="$2"
|
||||
shift 2
|
||||
local db_json="$1"
|
||||
local manifest="$payload_dir/manifest.json"
|
||||
local version
|
||||
version="$(mysql_alt_version)"
|
||||
mysql_load_alt_runtime
|
||||
|
||||
cat > "$manifest" <<JSON
|
||||
{
|
||||
"format": "da-alt-mysql-v1",
|
||||
"created_at": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')",
|
||||
"da_user": $(json_string "$da_user"),
|
||||
"mariadb_version": $(json_string "$version"),
|
||||
"source": "alt-mariadb",
|
||||
"source_instance": {
|
||||
"service": $(json_string "$MYSQL_ALT_SERVICE_NAME"),
|
||||
"basedir": $(json_string "$MYSQL_ALT_BASEDIR"),
|
||||
"datadir": $(json_string "$MYSQL_ALT_DATADIR"),
|
||||
"port": $(json_string "$MYSQL_ALT_PORT"),
|
||||
"socket": $(json_string "$MYSQL_ALT_SOCKET")
|
||||
},
|
||||
"databases": [
|
||||
$db_json
|
||||
]
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
||||
main() {
|
||||
local da_user backup_root payload_dir db_dir grants_dir metadata_dir
|
||||
local db db_file rel_file sha db_entries entry prefix where
|
||||
da_user="$(detect_da_user "${1:-}")"
|
||||
backup_root="$(detect_backup_root)"
|
||||
payload_dir="$backup_root/backup/alt_mysql"
|
||||
db_dir="$payload_dir/databases"
|
||||
grants_dir="$payload_dir/grants"
|
||||
metadata_dir="$payload_dir/metadata"
|
||||
|
||||
log "backup hook env: user=$da_user pwd=$(pwd -P) file=${file:-} filename=${filename:-} root=$backup_root"
|
||||
|
||||
mysql_load_alt_credentials
|
||||
mysql_ensure_metadata_schema
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
printf 'Would write alt-mysql payload for %s into %s\n' "$da_user" "$payload_dir"
|
||||
list_user_databases "$da_user"
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -rf "$payload_dir"
|
||||
mkdir -p "$db_dir" "$grants_dir" "$metadata_dir"
|
||||
|
||||
db_entries=""
|
||||
while IFS= read -r db; do
|
||||
[ -n "$db" ] || continue
|
||||
db_file="$db_dir/${db}.sql.gz"
|
||||
mysqldump_exec \
|
||||
--single-transaction \
|
||||
--quick \
|
||||
--skip-lock-tables \
|
||||
--routines \
|
||||
--triggers \
|
||||
--events \
|
||||
--default-character-set=utf8mb4 \
|
||||
"$db" | gzip -9 > "$db_file"
|
||||
rel_file="databases/${db}.sql.gz"
|
||||
sha="$(sha256_file "$db_file")"
|
||||
entry=" { \"name\": $(json_string "$db"), \"file\": $(json_string "$rel_file"), \"sha256\": $(json_string "$sha") }"
|
||||
if [ -n "$db_entries" ]; then
|
||||
db_entries+=$',\n'
|
||||
fi
|
||||
db_entries+="$entry"
|
||||
log "added database dump: $db"
|
||||
done < <(list_user_databases "$da_user")
|
||||
|
||||
dump_users_file "$da_user" "$grants_dir/users.sql.gz"
|
||||
dump_grants_file "$da_user" "$grants_dir/grants.sql.gz"
|
||||
|
||||
prefix="$(mysql_escape_literal "$da_user")"
|
||||
where="db_name LIKE '${prefix}\\_%'"
|
||||
dump_metadata_table da_plugin_database_owners "$where" "$metadata_dir/da_plugin_database_owners.sql.gz"
|
||||
dump_metadata_table da_plugin_grant_profiles "$where" "$metadata_dir/da_plugin_grant_profiles.sql.gz"
|
||||
where="da_user = '${prefix}' OR role_name LIKE '${prefix}\\_%'"
|
||||
dump_metadata_table da_plugin_access_hosts "$where" "$metadata_dir/da_plugin_access_hosts.sql.gz"
|
||||
|
||||
write_manifest "$da_user" "$payload_dir" "$db_entries"
|
||||
chmod -R go-rwx "$payload_dir" 2>/dev/null || true
|
||||
log "backup payload completed: $payload_dir"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
SETTINGS_FILE="$PLUGIN_DIR/plugin-settings.conf"
|
||||
LOG_FILE="${DA_ALT_MYSQL_RESTORE_LOG:-$PLUGIN_DIR/data/logs/da-restore.log}"
|
||||
RESTORE_PAYLOAD_SCRIPT="$SCRIPT_DIR/alt_mysql_restore_payload.sh"
|
||||
NATIVE_MIGRATE_SCRIPT="$SCRIPT_DIR/da_restore_native_staging_to_alt_mysql.sh"
|
||||
|
||||
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 hook: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
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}"
|
||||
}
|
||||
|
||||
detect_da_user() {
|
||||
local value="${DA_USER:-${username:-${user:-${USERNAME:-}}}}"
|
||||
if [ -z "$value" ]; then
|
||||
value="${1:-}"
|
||||
fi
|
||||
if [[ ! "$value" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]]; then
|
||||
die "cannot detect safe DirectAdmin username"
|
||||
fi
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
candidate_dir_if_valid() {
|
||||
local candidate="${1:-}"
|
||||
[ -n "$candidate" ] || return 1
|
||||
[ -d "$candidate" ] || return 1
|
||||
printf '%s\n' "$(cd "$candidate" && pwd -P)"
|
||||
}
|
||||
|
||||
find_payload_in_dir() {
|
||||
local root="$1"
|
||||
local payload=""
|
||||
[ -d "$root" ] || return 1
|
||||
for payload in \
|
||||
"$root/backup/alt_mysql" \
|
||||
"$root/alt_mysql" \
|
||||
"$root/mysql/backup/alt_mysql"; do
|
||||
if [ -r "$payload/manifest.json" ]; then
|
||||
printf '%s\n' "$payload"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
payload="$(find "$root" -maxdepth 5 -type f -path '*/backup/alt_mysql/manifest.json' -print -quit 2>/dev/null || true)"
|
||||
if [ -n "$payload" ]; then
|
||||
dirname "$payload"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_payload_from_archive() {
|
||||
local archive="$1"
|
||||
local tmp_dir payload_member
|
||||
[ -r "$archive" ] || return 1
|
||||
command -v tar >/dev/null 2>&1 || return 1
|
||||
|
||||
payload_member="$(tar -tf "$archive" 2>/dev/null | grep -E '(^|/)backup/alt_mysql/manifest\.json$' | head -n 1 || true)"
|
||||
[ -n "$payload_member" ] || return 1
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
tar -xf "$archive" -C "$tmp_dir" --wildcards '*/backup/alt_mysql/*' 'backup/alt_mysql/*' 2>/dev/null \
|
||||
|| tar -xf "$archive" -C "$tmp_dir" 2>/dev/null
|
||||
|
||||
find_payload_in_dir "$tmp_dir"
|
||||
}
|
||||
|
||||
detect_payload_dir() {
|
||||
local current candidate dir archive payload
|
||||
current="$(pwd -P)"
|
||||
for candidate in \
|
||||
"${DA_ALT_MYSQL_RESTORE_ROOT:-}" \
|
||||
"${restore_path:-}" \
|
||||
"${restore_dir:-}" \
|
||||
"${tmpdir:-}" \
|
||||
"${backup_path:-}" \
|
||||
"$current"; do
|
||||
if dir="$(candidate_dir_if_valid "$candidate")"; then
|
||||
if payload="$(find_payload_in_dir "$dir")"; then
|
||||
printf '%s\n' "$payload"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
for archive in "${filename:-}" "${file:-}" "${backup_file:-}"; do
|
||||
[ -n "$archive" ] || continue
|
||||
if payload="$(extract_payload_from_archive "$archive")"; then
|
||||
printf '%s\n' "$payload"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
local da_user enabled overwrite payload native_mode
|
||||
da_user="$(detect_da_user "${1:-}")"
|
||||
enabled="$(read_plugin_setting DA_ALT_MYSQL_RESTORE_ENABLED true)"
|
||||
native_mode="$(read_plugin_setting DA_ALT_MYSQL_NATIVE_STAGING_MIGRATE true)"
|
||||
|
||||
log "restore hook env: user=$da_user pwd=$(pwd -P) file=${file:-} filename=${filename:-}"
|
||||
|
||||
case "$enabled" in
|
||||
true|yes|on|1) ;;
|
||||
*) log "restore hook disabled by DA_ALT_MYSQL_RESTORE_ENABLED=$enabled"; return 0 ;;
|
||||
esac
|
||||
|
||||
overwrite="$(read_plugin_setting DA_ALT_MYSQL_RESTORE_OVERWRITE allow)"
|
||||
|
||||
if payload="$(detect_payload_dir)"; then
|
||||
[ -x "$RESTORE_PAYLOAD_SCRIPT" ] || die "restore payload script is not executable: $RESTORE_PAYLOAD_SCRIPT"
|
||||
log "plugin payload detected: $payload"
|
||||
"$RESTORE_PAYLOAD_SCRIPT" --user "$da_user" --payload "$payload" --overwrite "$overwrite"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "plugin payload not found; considering native staging migration"
|
||||
case "$native_mode" in
|
||||
true|yes|on|1)
|
||||
[ -x "$NATIVE_MIGRATE_SCRIPT" ] || die "native staging migration script is not executable: $NATIVE_MIGRATE_SCRIPT"
|
||||
"$NATIVE_MIGRATE_SCRIPT" --user "$da_user"
|
||||
;;
|
||||
*)
|
||||
log "native staging migration disabled; account restore continues without alt-mysql DB migration"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
SETTINGS_FILE="$PLUGIN_DIR/plugin-settings.conf"
|
||||
LOG_FILE="${DA_ALT_MYSQL_DB_HOOK_LOG:-$PLUGIN_DIR/data/logs/da-db-hooks.log}"
|
||||
HOOK_NAME="${DA_HOOK_NAME:-$(basename "$0")}"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_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}"
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[%s] hook=%s username=%s database=%s user=%s %s\n' \
|
||||
"$(date '+%Y-%m-%d %H:%M:%S')" \
|
||||
"$HOOK_NAME" \
|
||||
"${username:-}" \
|
||||
"${database:-${name:-}}" \
|
||||
"${user:-}" \
|
||||
"$*" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
enabled="$(read_plugin_setting DA_ALT_MYSQL_BLOCK_NATIVE_DB_HOOKS true)"
|
||||
case "$enabled" in
|
||||
true|yes|on|1) ;;
|
||||
*) log "native DB hook blocker disabled"; exit 0 ;;
|
||||
esac
|
||||
|
||||
if [ "${DA_ALT_MYSQL_ALLOW_NATIVE_DB_ACTION:-0}" = "1" ]; then
|
||||
log "native DB action explicitly allowed by environment"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$HOOK_NAME" in
|
||||
*_post.sh|*_post)
|
||||
log "native DirectAdmin database action observed after completion"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
log "blocked native DirectAdmin database action"
|
||||
cat >&2 <<'EOF'
|
||||
Databases on this server are managed by the MySQL plugin and the alt-mariadb instance.
|
||||
Use the MySQL plugin instead of DirectAdmin native database actions.
|
||||
EOF
|
||||
exit 1
|
||||
@@ -0,0 +1,448 @@
|
||||
#!/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="$PLUGIN_DIR/plugin-settings.conf"
|
||||
LOG_FILE="${DA_ALT_MYSQL_RESTORE_LOG:-$PLUGIN_DIR/data/logs/da-restore.log}"
|
||||
DA_USER=""
|
||||
NATIVE_MY_CNF="${NATIVE_MY_CNF:-/usr/local/directadmin/conf/my.cnf}"
|
||||
OVERWRITE_POLICY=""
|
||||
CLEANUP_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 'native staging to alt-mysql: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage: da_restore_native_staging_to_alt_mysql.sh --user DA_USER [--native-my-cnf /path] [--overwrite allow|deny] [--cleanup after_success|keep]
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--user) DA_USER="${2:-}"; shift 2 ;;
|
||||
--native-my-cnf) NATIVE_MY_CNF="${2:-}"; shift 2 ;;
|
||||
--overwrite) OVERWRITE_POLICY="${2:-}"; shift 2 ;;
|
||||
--cleanup) CLEANUP_POLICY="${2:-}"; shift 2 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$DA_USER" ] || usage
|
||||
[[ "$DA_USER" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]] || die "unsafe DirectAdmin username: $DA_USER"
|
||||
[ -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}"
|
||||
}
|
||||
|
||||
native_mysql() {
|
||||
"$NATIVE_MYSQL_BIN" --defaults-extra-file="$NATIVE_MY_CNF" "$@"
|
||||
}
|
||||
|
||||
native_query() {
|
||||
local database="$1"
|
||||
local sql="$2"
|
||||
native_mysql --batch --skip-column-names "$database" -e "$sql"
|
||||
}
|
||||
|
||||
safe_identifier() {
|
||||
local value="$1"
|
||||
[[ "$value" =~ ^[A-Za-z0-9_]+$ ]] || die "unsafe MySQL identifier: $value"
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
list_native_databases() {
|
||||
native_query information_schema "
|
||||
SELECT SCHEMA_NAME
|
||||
FROM SCHEMATA
|
||||
WHERE SCHEMA_NAME LIKE '$(mysql_escape_literal "$DA_USER")\\_%' ESCAPE '\\'
|
||||
ORDER BY SCHEMA_NAME
|
||||
" | grep -Ev '^(information_schema|performance_schema|mysql|sys)$' || true
|
||||
}
|
||||
|
||||
list_native_user_hosts() {
|
||||
native_query mysql "
|
||||
SELECT User, Host
|
||||
FROM user
|
||||
WHERE User LIKE '$(mysql_escape_literal "$DA_USER")\\_%' ESCAPE '\\'
|
||||
AND User <> ''
|
||||
ORDER BY User, Host
|
||||
" || true
|
||||
}
|
||||
|
||||
list_native_roles_for_db() {
|
||||
local db="$1"
|
||||
native_query mysql "
|
||||
SELECT DISTINCT User
|
||||
FROM db
|
||||
WHERE Db = '$(mysql_escape_literal "$db")'
|
||||
AND User LIKE '$(mysql_escape_literal "$DA_USER")\\_%' ESCAPE '\\'
|
||||
AND User <> ''
|
||||
ORDER BY User
|
||||
" || true
|
||||
}
|
||||
|
||||
native_show_create_user() {
|
||||
local user="$1"
|
||||
local host="$2"
|
||||
native_query mysql "SHOW CREATE USER '$(mysql_escape_literal "$user")'@'$(mysql_escape_literal "$host")'" | awk -F'\t' '{print $NF}'
|
||||
}
|
||||
|
||||
native_show_grants() {
|
||||
local user="$1"
|
||||
local host="$2"
|
||||
native_query mysql "SHOW GRANTS FOR '$(mysql_escape_literal "$user")'@'$(mysql_escape_literal "$host")'" || true
|
||||
}
|
||||
|
||||
apply_user_definition_to_alt() {
|
||||
local user="$1"
|
||||
local host="$2"
|
||||
local create_sql
|
||||
create_sql="$(native_show_create_user "$user" "$host" || true)"
|
||||
[ -n "$create_sql" ] || die "cannot recover password/authentication definition for ${user}@${host}"
|
||||
|
||||
{
|
||||
printf "DROP USER IF EXISTS '%s'@'%s';\n" "$(mysql_escape_literal "$user")" "$(mysql_escape_literal "$host")"
|
||||
printf '%s;\n' "$create_sql"
|
||||
} | mysql_exec mysql >> "$LOG_FILE" 2>&1 || die "cannot recreate user with preserved password: ${user}@${host}"
|
||||
}
|
||||
|
||||
apply_grants_to_alt() {
|
||||
local user="$1"
|
||||
local host="$2"
|
||||
local grant_line
|
||||
while IFS= read -r grant_line; do
|
||||
[ -n "$grant_line" ] || continue
|
||||
printf '%s;\n' "$grant_line" | mysql_exec mysql >> "$LOG_FILE" 2>&1 \
|
||||
|| die "cannot apply grant for ${user}@${host}"
|
||||
done < <(native_show_grants "$user" "$host")
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
db_privilege_profile() {
|
||||
local db="$1"
|
||||
local role="$2"
|
||||
local row privs=()
|
||||
row="$(native_query mysql "
|
||||
SELECT Select_priv, Insert_priv, Update_priv, Delete_priv, Create_priv, Drop_priv, Index_priv, Alter_priv,
|
||||
Create_tmp_table_priv, Lock_tables_priv, Create_view_priv, Show_view_priv, Create_routine_priv,
|
||||
Alter_routine_priv, Execute_priv, Event_priv, Trigger_priv
|
||||
FROM db
|
||||
WHERE Db='$(mysql_escape_literal "$db")'
|
||||
AND User='$(mysql_escape_literal "$role")'
|
||||
LIMIT 1
|
||||
" | head -n 1 || true)"
|
||||
|
||||
if [ -z "$row" ]; then
|
||||
printf 'ALL\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
IFS=$'\t' read -r select_p insert_p update_p delete_p create_p drop_p index_p alter_p tmp_p lock_p create_view_p show_view_p create_routine_p alter_routine_p execute_p event_p trigger_p <<< "$row"
|
||||
[ "${select_p:-N}" = "Y" ] && privs+=("SELECT")
|
||||
[ "${insert_p:-N}" = "Y" ] && privs+=("INSERT")
|
||||
[ "${update_p:-N}" = "Y" ] && privs+=("UPDATE")
|
||||
[ "${delete_p:-N}" = "Y" ] && privs+=("DELETE")
|
||||
[ "${create_p:-N}" = "Y" ] && privs+=("CREATE")
|
||||
[ "${drop_p:-N}" = "Y" ] && privs+=("DROP")
|
||||
[ "${index_p:-N}" = "Y" ] && privs+=("INDEX")
|
||||
[ "${alter_p:-N}" = "Y" ] && privs+=("ALTER")
|
||||
[ "${tmp_p:-N}" = "Y" ] && privs+=("CREATE TEMPORARY TABLES")
|
||||
[ "${lock_p:-N}" = "Y" ] && privs+=("LOCK TABLES")
|
||||
[ "${create_view_p:-N}" = "Y" ] && privs+=("CREATE VIEW")
|
||||
[ "${show_view_p:-N}" = "Y" ] && privs+=("SHOW VIEW")
|
||||
[ "${create_routine_p:-N}" = "Y" ] && privs+=("CREATE ROUTINE")
|
||||
[ "${alter_routine_p:-N}" = "Y" ] && privs+=("ALTER ROUTINE")
|
||||
[ "${execute_p:-N}" = "Y" ] && privs+=("EXECUTE")
|
||||
[ "${event_p:-N}" = "Y" ] && privs+=("EVENT")
|
||||
[ "${trigger_p:-N}" = "Y" ] && privs+=("TRIGGER")
|
||||
|
||||
if [ "${#privs[@]}" -eq 0 ]; then
|
||||
printf 'ALL\n'
|
||||
else
|
||||
local IFS=,
|
||||
printf '%s\n' "${privs[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
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}\\_%';"
|
||||
}
|
||||
|
||||
rebuild_metadata_for_db() {
|
||||
local db="$1"
|
||||
local owner="" role profile host
|
||||
while IFS= read -r role; do
|
||||
[ -n "$role" ] || continue
|
||||
if [ -z "$owner" ]; then
|
||||
owner="$role"
|
||||
fi
|
||||
profile="$(db_privilege_profile "$db" "$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 "$profile")')
|
||||
ON DUPLICATE KEY UPDATE privileges=VALUES(privileges), updated_at=CURRENT_TIMESTAMP;
|
||||
"
|
||||
while IFS=$'\t' read -r _user host; do
|
||||
[ -n "${host:-}" ] || continue
|
||||
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")', 'migrated from native DirectAdmin restore')
|
||||
ON DUPLICATE KEY UPDATE da_user=VALUES(da_user), note=VALUES(note), updated_at=CURRENT_TIMESTAMP;
|
||||
"
|
||||
done < <(list_native_user_hosts | awk -F'\t' -v role="$role" '$1 == role { print $0 }')
|
||||
done < <(list_native_roles_for_db "$db")
|
||||
|
||||
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;
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
verify_alt_database() {
|
||||
local db="$1"
|
||||
[ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ] \
|
||||
|| die "alt database missing after import: $db"
|
||||
}
|
||||
|
||||
drop_native_staging() {
|
||||
local db user host remaining
|
||||
log "cleanup native staging for $DA_USER"
|
||||
while IFS= read -r db; do
|
||||
[ -n "$db" ] || continue
|
||||
native_query mysql "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db")" >> "$LOG_FILE" 2>&1 || die "cannot drop native staged database: $db"
|
||||
done < <(list_native_databases)
|
||||
|
||||
while IFS=$'\t' read -r user host; do
|
||||
[ -n "${user:-}" ] || continue
|
||||
[ -n "${host:-}" ] || continue
|
||||
remaining="$(native_query mysql "
|
||||
SELECT COUNT(*)
|
||||
FROM db
|
||||
WHERE User='$(mysql_escape_literal "$user")'
|
||||
AND Db NOT LIKE '$(mysql_escape_literal "$DA_USER")\\_%' ESCAPE '\\'
|
||||
" | tail -n 1)"
|
||||
if [ "${remaining:-0}" = "0" ]; then
|
||||
native_query mysql "DROP USER IF EXISTS '$(mysql_escape_literal "$user")'@'$(mysql_escape_literal "$host")'" >> "$LOG_FILE" 2>&1 || true
|
||||
else
|
||||
log "keeping native user ${user}@${host}; it still has non-staging grants"
|
||||
fi
|
||||
done < <(list_native_user_hosts)
|
||||
}
|
||||
|
||||
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 native staging migration"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
local db tmp_dump user host overwrite cleanup native_count
|
||||
[ -r "$NATIVE_MY_CNF" ] || die "native DirectAdmin MySQL client config is not readable: $NATIVE_MY_CNF"
|
||||
NATIVE_MYSQL_BIN="$(mysql_native_binary mysql)" || die "native mysql/mariadb client not found"
|
||||
NATIVE_MYSQLDUMP_BIN="$(mysql_native_binary mysqldump)" || die "native mysqldump/mariadb-dump client not found"
|
||||
|
||||
mysql_load_alt_credentials
|
||||
mysql_ensure_metadata_schema
|
||||
native_mysql mysql -e "SELECT 1" >/dev/null 2>&1 || die "native DirectAdmin MySQL staging is not reachable"
|
||||
mysql_exec mysql -e "SELECT 1" >/dev/null 2>&1 || die "alt-mariadb is not reachable"
|
||||
|
||||
overwrite="${OVERWRITE_POLICY:-$(read_plugin_setting DA_ALT_MYSQL_RESTORE_OVERWRITE allow)}"
|
||||
cleanup="${CLEANUP_POLICY:-$(read_plugin_setting DA_ALT_MYSQL_NATIVE_CLEANUP after_success)}"
|
||||
case "$overwrite" in allow|deny) ;; prompt) overwrite="deny" ;; *) overwrite="deny" ;; esac
|
||||
case "$cleanup" in after_success|keep) ;; *) cleanup="keep" ;; esac
|
||||
|
||||
native_count="$(list_native_databases | wc -l | tr -d '[:space:]')"
|
||||
if [ "${native_count:-0}" = "0" ]; then
|
||||
log "no native staged databases for $DA_USER"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "native staging migration start: user=$DA_USER db_count=$native_count overwrite=$overwrite cleanup=$cleanup"
|
||||
|
||||
if [ "$overwrite" = "deny" ]; then
|
||||
while IFS= read -r db; do
|
||||
[ -n "$db" ] || continue
|
||||
if [ -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 < <(list_native_databases)
|
||||
fi
|
||||
|
||||
while IFS=$'\t' read -r user host; do
|
||||
[ -n "${user:-}" ] || continue
|
||||
[ -n "${host:-}" ] || continue
|
||||
log "preserving native user definition: ${user}@${host}"
|
||||
apply_user_definition_to_alt "$user" "$host"
|
||||
done < <(list_native_user_hosts)
|
||||
|
||||
metadata_cleanup_for_user
|
||||
|
||||
while IFS= read -r db; do
|
||||
[ -n "$db" ] || continue
|
||||
db="$(safe_identifier "$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
|
||||
|
||||
tmp_dump="$(mktemp)"
|
||||
log "dumping native staged database: $db"
|
||||
"$NATIVE_MYSQLDUMP_BIN" --defaults-extra-file="$NATIVE_MY_CNF" \
|
||||
--single-transaction \
|
||||
--quick \
|
||||
--skip-lock-tables \
|
||||
--routines \
|
||||
--triggers \
|
||||
--events \
|
||||
--default-character-set=utf8mb4 \
|
||||
"$db" > "$tmp_dump" 2>>"$LOG_FILE" || {
|
||||
rm -f "$tmp_dump"
|
||||
die "cannot dump native staged database: $db"
|
||||
}
|
||||
|
||||
log "importing database into alt-mariadb: $db"
|
||||
backup_alt_database_for_rollback "$db" || {
|
||||
rm -f "$tmp_dump"
|
||||
die "cannot create rollback dump before importing native staged 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
|
||||
rm -f "$tmp_dump"
|
||||
die "cannot drop target database before importing native staged database: $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
|
||||
rm -f "$tmp_dump"
|
||||
die "cannot create target database before importing native staged database: $db"
|
||||
fi
|
||||
mysql_exec "$db" < "$tmp_dump" >> "$LOG_FILE" 2>&1 || {
|
||||
restore_alt_database_from_rollback "$db" || true
|
||||
rm -f "$tmp_dump"
|
||||
die "cannot import native staged database into alt-mariadb: $db"
|
||||
}
|
||||
rm -f "$tmp_dump"
|
||||
cleanup_restore_rollback
|
||||
verify_alt_database "$db"
|
||||
rebuild_metadata_for_db "$db"
|
||||
done < <(list_native_databases)
|
||||
|
||||
while IFS=$'\t' read -r user host; do
|
||||
[ -n "${user:-}" ] || continue
|
||||
[ -n "${host:-}" ] || continue
|
||||
log "applying preserved grants: ${user}@${host}"
|
||||
apply_grants_to_alt "$user" "$host"
|
||||
done < <(list_native_user_hosts)
|
||||
|
||||
mysql_exec mysql -e "FLUSH PRIVILEGES;" >> "$LOG_FILE" 2>&1 || true
|
||||
sync_remote_hosts_if_enabled
|
||||
|
||||
if [ "$cleanup" = "after_success" ]; then
|
||||
drop_native_staging
|
||||
else
|
||||
log "native staging cleanup disabled by policy"
|
||||
fi
|
||||
|
||||
log "native staging migration completed: user=$DA_USER"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user