Harden scripts and restore workflow

This commit is contained in:
2026-01-30 16:35:40 +01:00
parent 4a905eeaaa
commit eb3264ba0f
3 changed files with 289 additions and 0 deletions

233
scripts/restore.sh Executable file
View 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."