114 lines
4.9 KiB
Bash
Executable file
114 lines
4.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# =====================================================================================
|
|
# Disaster recovery: bring the domain back from an online backup archive.
|
|
#
|
|
# ./scripts/restore.sh backups/<archive>.tar.bz2 <new-dc-hostname> [new-ip]
|
|
#
|
|
# HOW SAMBA RESTORE ACTUALLY WORKS (this surprises people)
|
|
# `samba-tool domain backup restore` performs a *join*: it unpacks the database into a
|
|
# target dir and then ADDS the target host as a NEW domain controller. Restoring under the
|
|
# same server name that the archive was taken from fails with
|
|
# "Entry CN=<NAME>,OU=Domain Controllers,... already exists"
|
|
# So: pick a NEW hostname (and an IP nothing else is using). The old DC name stays in DNS
|
|
# until you clean it up; point clients at the new DC.
|
|
#
|
|
# Sequence: backup .env -> stop live DC -> move data dir aside (never deleted) ->
|
|
# restore into fresh tree -> rewrite .env identity -> up -> verify
|
|
# =====================================================================================
|
|
set -uo pipefail
|
|
cd "$(dirname "$0")/.."
|
|
[ -f .env ] || { echo "no .env in $(pwd) - restore expects the production env file" >&2; exit 2; }
|
|
. "$(dirname "$0")/envfile.sh"; ad_env_load .env # parsed, never executed (see envfile.sh)
|
|
|
|
BACKUP="${1:-}"
|
|
NEW_HOST="${2:-${AD_RESTORE_AS:-}}"
|
|
NEW_IP="${3:-${AD_RESTORE_IP:-$AD_IP}}"
|
|
|
|
[ -n "$BACKUP" ] && [ -r "$BACKUP" ] || { echo "usage: $0 <backup.tar.bz2> <new-dc-hostname> [new-ip]" >&2; exit 2; }
|
|
[ -n "$NEW_HOST" ] || { echo "A Samba restore lands as a NEW domain controller. Give it a hostname, e.g. dc3" >&2; exit 2; }
|
|
if [ "${NEW_HOST,,}" = "$(tar -f "$BACKUP" -O ./backup.txt 2>/dev/null | sed -n "s/.*using DC '\([^']*\)'.*/\1/p" | head -1 | cut -d. -f1 | tr 'A-Z' 'a-z')" ]; then
|
|
echo "refusing: '$NEW_HOST' is the hostname the backup was taken from" >&2
|
|
exit 2
|
|
fi
|
|
|
|
src_dc="$(tar -f "$BACKUP" -O ./backup.txt 2>/dev/null | sed -n "s/.*using DC '\([^']*\)'.*/\1/p" | head -1)"
|
|
echo "archive: $(basename "$BACKUP")"
|
|
echo "from DC: ${src_dc:-unknown}"
|
|
echo "restore as: ${NEW_HOST}.${AD_DNS_DOMAIN} @ ${NEW_IP}"
|
|
read -r -p "Type RESTORE to proceed: " confirm
|
|
[ "$confirm" = "RESTORE" ] || { echo "aborted"; exit 1; }
|
|
|
|
if [ -f "${BACKUP}.sha256" ] && ( cd "$(dirname "$BACKUP")" && sha256sum -c "$(basename "$BACKUP").sha256" ); then
|
|
echo ">> archive checksum OK"
|
|
else
|
|
echo ">> no checksum (or mismatch) - continuing, you were warned"
|
|
fi
|
|
|
|
ts="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
cid="${AD_CONTAINER:-ad-dc}"
|
|
|
|
echo ">> stopping live DC"
|
|
docker compose stop ad-dc || true
|
|
|
|
echo ">> preserving current data/config so nothing is destroyed"
|
|
[ -f .env ] && cp -a .env ".env.pre-restore-${ts}"
|
|
if [ -d data/samba ] && [ -n "$(ls -A data/samba 2>/dev/null || true)" ]; then
|
|
mv data/samba "data/samba.pre-restore-${ts}"
|
|
echo " data/samba -> data/samba.pre-restore-${ts}"
|
|
fi
|
|
mkdir -p data/samba
|
|
|
|
echo ">> rewriting identity in .env (AD_HOSTNAME=${NEW_HOST}, AD_IP=${NEW_IP})"
|
|
cp -a .env .env.tmp
|
|
sed -i "s/^AD_HOSTNAME=.*/AD_HOSTNAME=${NEW_HOST}/; s/^AD_IP=.*/AD_IP=${NEW_IP}/" .env.tmp
|
|
mv .env.tmp .env
|
|
|
|
echo ">> restoring into the fresh tree (one-shot container; does not start the DC)"
|
|
docker run --rm \
|
|
--name "ad-restore-${ts}" \
|
|
--cap-drop ALL \
|
|
--cap-add SYS_ADMIN --cap-add CHOWN --cap-add FOWNER --cap-add DAC_OVERRIDE \
|
|
--cap-add SETUID --cap-add SETGID --cap-add NET_BIND_SERVICE \
|
|
-v "$PWD/$(dirname "$BACKUP"):/restore:ro" \
|
|
-v "$PWD/data/samba:/var/lib/samba" \
|
|
-v "$PWD/conf:/etc/samba" \
|
|
-v /etc/localtime:/etc/localtime:ro \
|
|
--env-file .env \
|
|
-e "AD_RESTORE_FROM=/restore/$(basename "$BACKUP")" \
|
|
-e AD_RESTORE_CONFIRM=yes \
|
|
"${AD_RESTORE_IMAGE:-local/samba-ad-dc:4.22-debian13}" || {
|
|
echo "!! restore failed - roll back with: cp .env.pre-restore-${ts} .env" >&2
|
|
exit 1
|
|
}
|
|
|
|
echo ">> starting the restored DC"
|
|
docker compose up -d ad-dc
|
|
|
|
echo ">> waiting for health (max 200s)"
|
|
h=none
|
|
for _ in $(seq 1 50); do
|
|
h="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid" 2>/dev/null || echo none)"
|
|
[ "$h" = healthy ] && break
|
|
sleep 4
|
|
done
|
|
echo " health: $h"
|
|
|
|
AD_CONTAINER="$cid" ./scripts/verify.sh || {
|
|
echo "verification failed. Roll back:"
|
|
echo " docker compose down"
|
|
echo " cp .env.pre-restore-${ts} .env"
|
|
echo " rm -rf data/samba && mv data/samba.pre-restore-${ts} data/samba"
|
|
exit 1
|
|
}
|
|
|
|
cat <<EOF
|
|
|
|
Restored as ${NEW_HOST}.${AD_DNS_DOMAIN} (${NEW_IP}).
|
|
Remaining manual steps:
|
|
1. DHCP/DNS: hand clients the new DC address (${NEW_IP}) - option 006.
|
|
2. If the old DC object (${src_dc:-unknown}) is still in the directory and will never
|
|
come back, remove its stale computer/DNS objects so clients stop trying to reach it:
|
|
docker exec -it $cid samba-tool dns serverdelete 127.0.0.1 <old-fqdn> -U administrator
|
|
docker exec -it $cid ldbdel -H /var/lib/samba/private/sam.ldb "<old computer DN>" # careful
|
|
3. Take a fresh backup immediately: make backup
|
|
EOF
|