First Commit

This commit is contained in:
Lonny Lowe 2026-09-24 11:29:54 -05:00
commit 2e5a4751a7
30 changed files with 2517 additions and 0 deletions

114
scripts/restore.sh Executable file
View file

@ -0,0 +1,114 @@
#!/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