234 lines
6.7 KiB
Bash
Executable File
234 lines
6.7 KiB
Bash
Executable File
#!/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."
|