Add MySQL grants backup and MySQL restore support

- mysql_dump_grants(): backs up user grants via SHOW CREATE USER + SHOW GRANTS,
  skipping system users (root, mysql.sys, etc.)
- mysql_restore_databases(): restores .sql.gz dumps and grants.sql from snapshot
- Backup flow: grants dumped alongside database dumps into _mysql/
- Restore flow: automatically restores MySQL databases and grants when _mysql/
  exists in snapshot and target has MySQL enabled
- CLI: --skip-mysql flag to opt out of MySQL restore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shuki
2026-03-06 04:53:41 +02:00
parent 75fd25e559
commit c1d00108ba
4 changed files with 207 additions and 0 deletions

View File

@@ -158,6 +158,14 @@ run_cli() {
dest=$(_parse_flag "--dest" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
folder=$(_parse_flag "--folder" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
# Check for --skip-mysql flag
local skip_mysql=""
local arg
for arg in "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}"; do
[[ "$arg" == "--skip-mysql" ]] && skip_mysql="yes"
done
[[ -n "$skip_mysql" ]] && export SKIP_MYSQL_RESTORE="yes"
[[ -z "$target" ]] && die "restore requires --target=NAME"
if [[ -z "$remote" ]]; then

View File

@@ -106,6 +106,7 @@ _backup_target_impl() {
if [[ "${TARGET_MYSQL_ENABLED:-no}" == "yes" ]]; then
log_info "Dumping MySQL databases for $target_name..."
if mysql_dump_databases; then
mysql_dump_grants || log_warn "Grants dump failed, continuing with database dumps"
mysql_dump_dir="${MYSQL_DUMP_DIR:-}"
else
log_error "MySQL dump failed for $target_name"

View File

@@ -190,6 +190,164 @@ mysql_dump_databases() {
return 0
}
# Dump MySQL user grants to grants.sql in the dump directory.
# Must be called after mysql_dump_databases() sets MYSQL_DUMP_DIR.
mysql_dump_grants() {
local client_cmd
client_cmd=$(_mysql_find_client_cmd) || {
log_error "MySQL/MariaDB client not found — cannot dump grants"
return 1
}
mysql_build_conn_args
local grants_file="$MYSQL_DUMP_DIR/_mysql/grants.sql"
# System users to skip
local -a skip_users=(
"'root'@'localhost'"
"'mysql.sys'@'localhost'"
"'mysql.infoschema'@'localhost'"
"'mysql.session'@'localhost'"
"'debian-sys-maint'@'localhost'"
"'mariadb.sys'@'localhost'"
)
# Get all users
local users_output
users_output=$("$client_cmd" "${MYSQL_CONN_ARGS[@]}" -N -e \
"SELECT CONCAT(\"'\", user, \"'@'\", host, \"'\") FROM mysql.user" 2>&1) || {
log_error "Failed to list MySQL users: $users_output"
return 1
}
local count=0
{
echo "-- MySQL grants dump"
echo "-- Generated: $(date -Iseconds)"
echo ""
while IFS= read -r user_host; do
user_host="${user_host#"${user_host%%[![:space:]]*}"}"
user_host="${user_host%"${user_host##*[![:space:]]}"}"
[[ -z "$user_host" ]] && continue
# Skip system users
local skip=false
local su
for su in "${skip_users[@]}"; do
if [[ "$user_host" == "$su" ]]; then
skip=true
break
fi
done
[[ "$skip" == "true" ]] && continue
# Try SHOW CREATE USER (MySQL 5.7+/MariaDB 10.2+)
local create_user
create_user=$("$client_cmd" "${MYSQL_CONN_ARGS[@]}" -N -e \
"SHOW CREATE USER $user_host" 2>/dev/null) || true
if [[ -n "$create_user" ]]; then
echo "$create_user;"
fi
# SHOW GRANTS
local grants
grants=$("$client_cmd" "${MYSQL_CONN_ARGS[@]}" -N -e \
"SHOW GRANTS FOR $user_host" 2>/dev/null) || continue
while IFS= read -r grant_line; do
[[ -n "$grant_line" ]] && echo "$grant_line;"
done <<< "$grants"
echo ""
((count++)) || true
done <<< "$users_output"
} > "$grants_file"
log_info "MySQL grants dumped: $count user(s) -> grants.sql"
return 0
}
# Restore MySQL databases from a directory of .sql.gz files.
# Usage: mysql_restore_databases <dir_path>
# The directory should contain *.sql.gz files and optionally grants.sql.
mysql_restore_databases() {
local mysql_dir="$1"
if [[ ! -d "$mysql_dir" ]]; then
log_error "MySQL restore dir not found: $mysql_dir"
return 1
fi
local client_cmd
client_cmd=$(_mysql_find_client_cmd) || {
log_error "MySQL/MariaDB client not found — cannot restore databases"
return 1
}
mysql_build_conn_args
local errors=0
# Restore database dumps
local f
for f in "$mysql_dir"/*.sql.gz; do
[[ -f "$f" ]] || continue
local db_name
db_name=$(basename "$f" .sql.gz)
# Skip system databases
local skip=false
local sdb
for sdb in $_MYSQL_SYSTEM_DBS; do
if [[ "$db_name" == "$sdb" ]]; then
skip=true
break
fi
done
[[ "$skip" == "true" ]] && continue
log_info "Restoring MySQL database: $db_name"
# Create database if not exists
"$client_cmd" "${MYSQL_CONN_ARGS[@]}" -e \
"CREATE DATABASE IF NOT EXISTS \`$db_name\`" 2>/dev/null || {
log_error "Failed to create database: $db_name"
((errors++)) || true
continue
}
# Import dump
if gunzip -c "$f" | "$client_cmd" "${MYSQL_CONN_ARGS[@]}" "$db_name" 2>/dev/null; then
log_info "Restored database: $db_name"
else
log_error "Failed to restore database: $db_name"
((errors++)) || true
fi
done
# Restore grants
if [[ -f "$mysql_dir/grants.sql" ]]; then
log_info "Restoring MySQL grants..."
if "$client_cmd" "${MYSQL_CONN_ARGS[@]}" < "$mysql_dir/grants.sql" 2>/dev/null; then
log_info "MySQL grants restored"
"$client_cmd" "${MYSQL_CONN_ARGS[@]}" -e "FLUSH PRIVILEGES" 2>/dev/null || true
else
log_error "Failed to restore some MySQL grants (partial restore may have occurred)"
((errors++)) || true
fi
fi
unset MYSQL_PWD 2>/dev/null || true
if (( errors > 0 )); then
log_error "MySQL restore completed with $errors error(s)"
return 1
fi
log_info "MySQL restore completed successfully"
return 0
}
# Clean up the temporary MySQL dump directory and env vars.
mysql_cleanup_dump() {
if [[ -n "${MYSQL_DUMP_DIR:-}" && -d "$MYSQL_DUMP_DIR" ]]; then

View File

@@ -99,6 +99,46 @@ restore_target() {
fi
done < <(get_target_folders)
# Restore MySQL databases if snapshot contains _mysql/ and target has MySQL enabled
if [[ "${TARGET_MYSQL_ENABLED:-no}" == "yes" && "${SKIP_MYSQL_RESTORE:-}" != "yes" ]]; then
log_info "Checking for MySQL dumps in snapshot..."
local mysql_restore_dir
mysql_restore_dir=$(mktemp -d "${WORK_DIR:-/tmp}/gniza-mysql-restore-XXXXXX")
mkdir -p "$mysql_restore_dir/_mysql"
local mysql_found=false
if _is_rclone_mode; then
local mysql_subpath="targets/${target_name}/snapshots/${ts}/_mysql"
if rclone_from_remote "$mysql_subpath" "$mysql_restore_dir/_mysql" 2>/dev/null; then
mysql_found=true
fi
elif [[ "${REMOTE_TYPE:-ssh}" == "local" ]]; then
local mysql_source="$snap_dir/$ts/_mysql/"
if [[ -d "$mysql_source" ]]; then
rsync -aHAX "$mysql_source" "$mysql_restore_dir/_mysql/" && mysql_found=true
fi
else
local mysql_source="$snap_dir/$ts/_mysql/"
if _rsync_download "$mysql_source" "$mysql_restore_dir/_mysql/" 2>/dev/null; then
mysql_found=true
fi
fi
if [[ "$mysql_found" == "true" ]] && ls "$mysql_restore_dir/_mysql/"*.sql.gz &>/dev/null || \
[[ -f "$mysql_restore_dir/_mysql/grants.sql" ]]; then
log_info "Found MySQL dumps in snapshot, restoring..."
if ! mysql_restore_databases "$mysql_restore_dir/_mysql"; then
log_error "MySQL restore had errors"
((errors++)) || true
fi
else
log_debug "No MySQL dumps found in snapshot"
fi
rm -rf "$mysql_restore_dir"
elif [[ "${SKIP_MYSQL_RESTORE:-}" == "yes" ]]; then
log_info "Skipping MySQL restore (--skip-mysql)"
fi
_restore_remote_globals
if (( errors > 0 )); then