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 "$@"
|
||||
Reference in New Issue
Block a user