Harden scripts and restore workflow
This commit is contained in:
233
scripts/restore.sh
Executable file
233
scripts/restore.sh
Executable file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
ts() { date +"%Y-%m-%d %H:%M:%S"; }
|
||||
log() { echo "[$(ts)] [INFO] $*"; }
|
||||
warn(){ echo "[$(ts)] [WARN] $*" >&2; }
|
||||
err() { echo "[$(ts)] [ERROR] $*" >&2; }
|
||||
die() { err "$*"; exit 1; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/restore.sh <backup_file.bak> <target_db_name> [options]
|
||||
|
||||
Options:
|
||||
--replace Overwrite existing database.
|
||||
--compat <level> Set compatibility level after restore (e.g. 170).
|
||||
--recovery <model> Set recovery model after restore: SIMPLE|FULL|BULK_LOGGED
|
||||
Default: SIMPLE (recommended for local dev).
|
||||
--no-tuning Skip post-restore tuning (compat/recovery changes).
|
||||
|
||||
Examples:
|
||||
./scripts/restore.sh KundenDB.bak KundenDB
|
||||
./scripts/restore.sh KundenDB.bak KundenDB --replace
|
||||
./scripts/restore.sh KundenDB.bak KundenDB --compat 170 --recovery SIMPLE
|
||||
./scripts/restore.sh KundenDB.bak KundenDB --no-tuning
|
||||
|
||||
Notes:
|
||||
- Put the .bak into ./backup/ (host) so it appears in the container as:
|
||||
/var/opt/mssql/backup/<backup_file.bak>
|
||||
- Requires a running, healthy container named "mssql2025".
|
||||
EOF
|
||||
}
|
||||
|
||||
dump_logs() {
|
||||
warn "Container logs (last 250 lines):"
|
||||
docker logs --tail=250 mssql2025 2>/dev/null || true
|
||||
}
|
||||
|
||||
on_error() {
|
||||
local code=$?
|
||||
err "Restore failed (exit code=$code)."
|
||||
dump_logs
|
||||
exit "$code"
|
||||
}
|
||||
trap on_error ERR
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# -------- Preflight --------
|
||||
command -v docker >/dev/null 2>&1 || die "docker not found in PATH."
|
||||
docker info >/dev/null 2>&1 || die "docker daemon not reachable."
|
||||
|
||||
[[ -f .env ]] || die "Missing .env. Create it first: cp .env.example .env"
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
set +a
|
||||
[[ -n "${MSSQL_SA_PASSWORD:-}" ]] || die "MSSQL_SA_PASSWORD is empty in .env"
|
||||
|
||||
# -------- Args --------
|
||||
[[ $# -ge 2 ]] || { usage; exit 1; }
|
||||
|
||||
bak_file="$1"
|
||||
target_db="$2"
|
||||
shift 2
|
||||
|
||||
replace="0"
|
||||
compat_level=""
|
||||
recovery_model="SIMPLE"
|
||||
do_tuning="1"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--replace)
|
||||
replace="1"; shift ;;
|
||||
--compat)
|
||||
[[ $# -ge 2 ]] || die "--compat requires a value (e.g. 170)"
|
||||
compat_level="$2"
|
||||
shift 2 ;;
|
||||
--recovery)
|
||||
[[ $# -ge 2 ]] || die "--recovery requires SIMPLE|FULL|BULK_LOGGED"
|
||||
recovery_model="$2"
|
||||
shift 2 ;;
|
||||
--no-tuning)
|
||||
do_tuning="0"; shift ;;
|
||||
-h|--help)
|
||||
usage; exit 0 ;;
|
||||
*)
|
||||
die "Unknown argument: $1 (use --help)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate recovery model
|
||||
case "$recovery_model" in
|
||||
SIMPLE|FULL|BULK_LOGGED) ;;
|
||||
*)
|
||||
die "Invalid --recovery value: '$recovery_model'. Use SIMPLE|FULL|BULK_LOGGED." ;;
|
||||
esac
|
||||
|
||||
# Validate compat level if provided (basic numeric check)
|
||||
if [[ -n "$compat_level" ]]; then
|
||||
[[ "$compat_level" =~ ^[0-9]+$ ]] || die "Invalid --compat value: '$compat_level' (must be numeric, e.g. 170)."
|
||||
fi
|
||||
|
||||
# Container must be running and healthy
|
||||
if ! docker ps --format '{{.Names}}' | grep -qx 'mssql2025'; then
|
||||
die "Container mssql2025 is not running. Start it first: ./scripts/start.sh"
|
||||
fi
|
||||
health="$(docker inspect -f '{{.State.Health.Status}}' mssql2025 2>/dev/null || true)"
|
||||
[[ "$health" == "healthy" ]] || die "Container is not healthy (health=$health). Run ./scripts/start.sh"
|
||||
|
||||
# Backup must exist on host
|
||||
host_bak_path="./backup/$bak_file"
|
||||
[[ -f "$host_bak_path" ]] || die "Backup file not found: $host_bak_path"
|
||||
|
||||
container_bak_path="/var/opt/mssql/backup/$bak_file"
|
||||
|
||||
log "Restore requested:"
|
||||
log " backup : $host_bak_path (container: $container_bak_path)"
|
||||
log " target : $target_db"
|
||||
log " replace : $replace"
|
||||
log " tuning : $do_tuning (recovery=$recovery_model${compat_level:+, compat=$compat_level})"
|
||||
|
||||
# -------- Get logical file names from backup --------
|
||||
log "Reading FILELISTONLY..."
|
||||
filelist=$(
|
||||
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||
-W -s"|" -h -1 \
|
||||
-Q "SET NOCOUNT ON; RESTORE FILELISTONLY FROM DISK = N'$container_bak_path';" \
|
||||
< /dev/null \
|
||||
| tr -d '\r'
|
||||
)
|
||||
|
||||
[[ -n "$filelist" ]] || die "RESTORE FILELISTONLY returned no output. Is the .bak readable?"
|
||||
|
||||
data_logical="$(echo "$filelist" | awk -F'|' '$3 ~ /^D$/ {print $1; exit}')"
|
||||
log_logical="$(echo "$filelist" | awk -F'|' '$3 ~ /^L$/ {print $1; exit}')"
|
||||
|
||||
[[ -n "$data_logical" ]] || die "Could not detect data logical name (Type=D) from FILELISTONLY."
|
||||
[[ -n "$log_logical" ]] || die "Could not detect log logical name (Type=L) from FILELISTONLY."
|
||||
|
||||
log "Detected logical names:"
|
||||
log " DATA: $data_logical"
|
||||
log " LOG : $log_logical"
|
||||
|
||||
# Destination files in container
|
||||
dest_mdf="/var/opt/mssql/data/${target_db}.mdf"
|
||||
dest_ldf="/var/opt/mssql/data/${target_db}_log.ldf"
|
||||
|
||||
# -------- Build RESTORE DATABASE command --------
|
||||
with_opts="MOVE N'$data_logical' TO N'$dest_mdf',
|
||||
MOVE N'$log_logical' TO N'$dest_ldf',
|
||||
STATS = 5"
|
||||
if [[ "$replace" == "1" ]]; then
|
||||
with_opts="$with_opts,
|
||||
REPLACE"
|
||||
fi
|
||||
|
||||
restore_sql=$(
|
||||
cat <<SQL
|
||||
RESTORE DATABASE [$target_db]
|
||||
FROM DISK = N'$container_bak_path'
|
||||
WITH
|
||||
$with_opts;
|
||||
SQL
|
||||
)
|
||||
|
||||
log "Starting restore (large DBs can take a while)..."
|
||||
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||
-b -Q "$restore_sql" < /dev/null
|
||||
|
||||
log "Restore finished ✅"
|
||||
|
||||
# -------- Post-restore info --------
|
||||
log "Post-restore database info:"
|
||||
info_sql=$(
|
||||
cat <<SQL
|
||||
SET NOCOUNT ON;
|
||||
SELECT
|
||||
name,
|
||||
state_desc,
|
||||
recovery_model_desc,
|
||||
compatibility_level
|
||||
FROM sys.databases
|
||||
WHERE name = N'$target_db';
|
||||
SQL
|
||||
)
|
||||
|
||||
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||
-W -h -1 -Q "$info_sql" < /dev/null \
|
||||
| tr -d '\r' \
|
||||
| while read -r line; do
|
||||
[[ -n "$line" ]] && log " $line"
|
||||
done
|
||||
|
||||
# -------- Optional tuning --------
|
||||
if [[ "$do_tuning" == "1" ]]; then
|
||||
# Set recovery model (default SIMPLE)
|
||||
log "Applying recovery model: $recovery_model"
|
||||
tune_sql="ALTER DATABASE [$target_db] SET RECOVERY $recovery_model WITH NO_WAIT;"
|
||||
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||
-b -Q "$tune_sql" < /dev/null
|
||||
|
||||
# Set compatibility level if requested
|
||||
if [[ -n "$compat_level" ]]; then
|
||||
log "Applying compatibility level: $compat_level"
|
||||
compat_sql="ALTER DATABASE [$target_db] SET COMPATIBILITY_LEVEL = $compat_level;"
|
||||
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||
-b -Q "$compat_sql" < /dev/null
|
||||
fi
|
||||
|
||||
log "Tuning applied ✅"
|
||||
else
|
||||
warn "Post-restore tuning skipped (--no-tuning)."
|
||||
fi
|
||||
|
||||
# -------- Final info --------
|
||||
log "Final database info:"
|
||||
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||
-W -h -1 -Q "$info_sql" < /dev/null \
|
||||
| tr -d '\r' \
|
||||
| while read -r line; do
|
||||
[[ -n "$line" ]] && log " $line"
|
||||
done
|
||||
|
||||
log "Done."
|
||||
Reference in New Issue
Block a user