Repo Restore
Odtwarzanie serwera z repozytorium
Skrypt repo-restore.sh odpowiada za odtworzenie wybranych plików konfiguracyjnych z repozytorium na host.
Zakres działania skryptu:
- odtworzenie definicji Dockera (Docker Compose oraz projekty) z repozytorium do katalogów roboczych
- odtworzenie wybranych plików i jednostek z
/etc,/etc/systemdoraz/etc/systemd/system - blokada równoległego uruchomienia (mechanizm lock)
- walidacja pliku konfiguracyjnego i wymaganych zmiennych środowiskowych
- zapis logów i zestawienia zmian do katalogu uruchomienia
- retencja katalogów uruchomień
Repozytorium jest traktowane jako źródło prawdy — pliki obecne w repozytorium nadpisują pliki systemowe.
Warning
Uruchomienie bez --dry-run wymaga ręcznego potwierdzenia poprzez wpisanie RESTORE.
Wymagany skrypt wspólny
Wymagane skrypty
Aby skrypt wykonał się poprawnie, w tym samym folderze musi znajdować się repo-common.sh, który zawiera funkcje współdzielone przez wszystkie skrypty synchronizacji i odtwarzania repo.
Wymagany plik konfiguracyjny repo.env
Wymagany plik konfiguracyjny
Plik repo.env jest wymagany do działania skryptów repo-sync.sh oraz repo-restore.sh.
Instrukcja utworzenia pliku znajduje się w dokumencie instalacji Git:
Tworzenie katalogów
Tworzymy katalog uruchomień:
sudo mkdir -p /srv/backups/repo/runs
Tworzenie skryptu
Tworzymy plik:
sudo micro /srv/config/scripts/repo-restore.sh
W pliku umieszczamy:
#!/usr/bin/env bash
# /srv/config/scripts/repo-restore.sh
# Restore selected config + docker definitions from repo back to the host.
set -euo pipefail
IFS=$'\n\t'
umask 027
# --- Load common library (robust path) ---
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
LIB_FILE="${SCRIPT_DIR}/repo-common.sh"
if [[ ! -f "${LIB_FILE}" ]]; then
echo "CRITICAL: Library ${LIB_FILE} not found." >&2
exit 1
fi
# shellcheck disable=SC1090
source "${LIB_FILE}"
# --- Defaults / paths ---
ENV_FILE="/srv/backups/repo/repo.env"
RUNS_BASE_DIR="/srv/backups/repo/runs"
RUN_RETENTION_DAYS_DEFAULT="30"
# --- CLI ---
DRY_RUN=0
TARGET=""
CLEAN_DELETE=0 # optional destructive mode (off by default)
usage() {
cat <<'EOF'
Usage: repo-restore.sh [--dry-run|-n] [--delete] {compose|projects|docker|etc|systemd|units|host|all}
--dry-run, -n Show what would change, do not write anything.
--delete Allow rsync to delete files not present in repo (docker restore only).
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run|-n) DRY_RUN=1; shift ;;
--delete) CLEAN_DELETE=1; shift ;;
-h|--help) usage; exit 0 ;;
*) TARGET="$1"; shift ;;
esac
done
TARGET="${TARGET:-host}"
# --- Run dir (created early) ---
TS="$(date +%Y%m%d_%H%M)"
RUN_DIR="${RUNS_BASE_DIR}/${TS}"
mkdir -p "${RUNS_BASE_DIR}"
setup_run_env "${RUN_DIR}"
LOG_FILE="${RUN_DIR}/restore.log"
CHANGES_FILE="${RUN_DIR}/changes.txt"
: > "${CHANGES_FILE}"
# Log everything to file + stdout (journald)
exec > >(tee -a "${LOG_FILE}") 2>&1
header "Repo Restore: ${TS}"
log "Target: ${TARGET} (dry_run=${DRY_RUN}, delete=${CLEAN_DELETE})"
# --- Preflight ---
check_root
acquire_lock "repo-restore"
require_cmd rsync
require_cmd stat
require_cmd flock
check_config_perms "${ENV_FILE}"
load_repo_env "${ENV_FILE}"
# --- Validate required env vars ---
: "${REPO_ROOT:?REPO_ROOT is missing in repo.env}"
: "${REPO_USER:?REPO_USER is missing in repo.env}"
: "${REPO_GROUP:?REPO_GROUP is missing in repo.env}"
: "${SOURCE_COMPOSE:?SOURCE_COMPOSE is missing in repo.env}"
: "${SOURCE_PROJECTS:?SOURCE_PROJECTS is missing in repo.env}"
# Optional policy (can be set in repo.env)
RUN_RETENTION_DAYS="${RUN_RETENTION_DAYS:-$RUN_RETENTION_DAYS_DEFAULT}"
# --- Safety stops ---
[[ "${REPO_ROOT}" != "/" ]] || critical "SAFETY STOP: REPO_ROOT is set to /"
require_under "/srv" "${REPO_ROOT}"
require_dir "${REPO_ROOT}"
require_under "/srv" "${SOURCE_COMPOSE}"
require_under "/srv" "${SOURCE_PROJECTS}"
# --- Metadata (no secrets) ---
write_env_summary "${RUN_DIR}" \
"ts=${TS}" \
"hostname=$(hostname -s)" \
"script=repo-restore" \
"target=${TARGET}" \
"dry_run=${DRY_RUN}" \
"delete=${CLEAN_DELETE}" \
"repo_root=${REPO_ROOT}" \
"source_compose=${SOURCE_COMPOSE}" \
"source_projects=${SOURCE_PROJECTS}" \
"run_retention_days=${RUN_RETENTION_DAYS}" \
"rsync_version=$(rsync --version 2>/dev/null | head -n1 || echo unknown)"
# ============================================================
# Safety prompt (manual confirmation)
# ============================================================
confirm_execution() {
header "Safety Confirmation"
echo -e "${RED}WARNING:${NC} You are about to OVERWRITE host files from the repository."
echo " Repo: ${REPO_ROOT}"
echo " Target: ${TARGET}"
if [[ "${TARGET}" =~ ^(compose|projects|docker|all)$ ]] && [[ "${CLEAN_DELETE}" -eq 1 ]]; then
echo -e " Mode: ${RED}--delete enabled${NC} (extra destructive)"
fi
echo ""
# Encourage preview
if [[ "${DRY_RUN}" -eq 0 ]]; then
echo "Tip: Run with --dry-run first to preview changes."
fi
# Strong confirmation
local expected="RESTORE"
read -r -p "Type ${expected} to proceed: " reply
if [[ "${reply}" != "${expected}" ]]; then
echo "Aborted by user."
exit 1
fi
}
# Always require manual confirmation for real runs (not for dry-run).
if [[ "${DRY_RUN}" -eq 0 ]]; then
confirm_execution
else
header "Dry-run mode"
log "No changes will be applied."
fi
# ============================================================
# Helpers
# ============================================================
# Base rsync args for restore operations (repo -> host)
rsync_restore() {
local src="$1"
local dst="$2"
require_nonempty "$src" "src"
require_nonempty "$dst" "dst"
local -a args=(
rsync
-a
--safe-links
--numeric-ids
--itemize-changes
--info=FLIST2,STATS2
)
# For docker restore we allow optional --delete
if [[ "${CLEAN_DELETE}" -eq 1 ]]; then
args+=(--delete --delete-delay)
fi
if [[ "${DRY_RUN}" -eq 1 ]]; then
args+=(--dry-run)
fi
[[ "$src" == */ ]] || src="${src}/"
[[ "$dst" == */ ]] || dst="${dst}/"
"${args[@]}" "$src" "$dst" 2>&1 | tee -a "${CHANGES_FILE}"
}
restore_file() {
local repo_rel="$1" # relative to REPO_ROOT
local sys_path="$2"
local owner="$3"
local perms="$4"
require_nonempty "$repo_rel" "repo_rel"
require_nonempty "$sys_path" "sys_path"
require_nonempty "$owner" "owner"
require_nonempty "$perms" "perms"
local repo_path="${REPO_ROOT}/${repo_rel}"
if [[ -f "$repo_path" ]]; then
if [[ "${DRY_RUN}" -eq 1 ]]; then
echo " DRY: would restore: ${sys_path} (${owner}, ${perms})"
return
fi
mkdir -p "$(dirname "$sys_path")"
cp -a -- "$repo_path" "$sys_path"
chown "$owner" "$sys_path"
chmod "$perms" "$sys_path"
echo " + Restored: $sys_path ($owner, $perms)"
else
echo -e " ${YELLOW}! Skipped (missing in repo): ${repo_rel}${NC}"
fi
}
# Wrappers (mirror repo-sync structure)
etc_restore() {
local rel_dir="$1"
shift
local repo_base="host/etc"
local sys_base="/etc"
if [[ "$rel_dir" != "." ]]; then
repo_base="host/etc/${rel_dir}"
sys_base="/etc/${rel_dir}"
fi
for f in "$@"; do
restore_file "${repo_base}/${f}" "${sys_base}/${f}" "root:root" "644"
done
}
systemd_restore() {
local rel_dir="$1"
shift
local repo_base="host/etc/systemd"
local sys_base="/etc/systemd"
if [[ "$rel_dir" != "." ]]; then
repo_base="host/etc/systemd/${rel_dir}"
sys_base="/etc/systemd/${rel_dir}"
fi
for f in "$@"; do
restore_file "${repo_base}/${f}" "${sys_base}/${f}" "root:root" "644"
done
}
units_restore() {
local rel_dir="$1"
shift
local repo_base="host/etc/systemd/system"
local sys_base="/etc/systemd/system"
if [[ "$rel_dir" != "." ]]; then
repo_base="host/etc/systemd/system/${rel_dir}"
sys_base="/etc/systemd/system/${rel_dir}"
fi
for f in "$@"; do
restore_file "${repo_base}/${f}" "${sys_base}/${f}" "root:root" "644"
done
}
# ============================================================
# Restore modules
# ============================================================
restore_compose() {
header "Restore: Docker Compose"
local src="${REPO_ROOT}/docker/compose"
local dst="${SOURCE_COMPOSE}"
if [[ ! -d "$src" ]]; then
warn "Repo compose directory missing: $src (skipping)"
return
fi
if [[ -z "$dst" ]] || [[ ! -d "$dst" ]]; then
warn "SOURCE_COMPOSE missing/unset: $dst (skipping)"
return
fi
# Compose/projects are owned by REPO_USER on the host.
if [[ "${DRY_RUN}" -eq 0 ]]; then
mkdir -p "$dst"
fi
log "Rsync: ${src} -> ${dst}"
rsync_restore "$src" "$dst"
if [[ "${DRY_RUN}" -eq 0 ]]; then
chown -R "${REPO_USER}:${REPO_GROUP}" "$dst"
else
log "DRY: would chown -R ${REPO_USER}:${REPO_GROUP} ${dst}"
fi
log "Compose files updated."
}
restore_projects() {
header "Restore: Docker Projects"
local src="${REPO_ROOT}/docker/projects"
local dst="${SOURCE_PROJECTS}"
if [[ ! -d "$src" ]]; then
warn "Repo projects directory missing: $src (skipping)"
return
fi
if [[ -z "$dst" ]] || [[ ! -d "$dst" ]]; then
warn "SOURCE_PROJECTS missing/unset: $dst (skipping)"
return
fi
if [[ "${DRY_RUN}" -eq 0 ]]; then
mkdir -p "$dst"
fi
log "Rsync: ${src} -> ${dst}"
rsync_restore "$src" "$dst"
if [[ "${DRY_RUN}" -eq 0 ]]; then
chown -R "${REPO_USER}:${REPO_GROUP}" "$dst"
else
log "DRY: would chown -R ${REPO_USER}:${REPO_GROUP} ${dst}"
fi
log "Projects files updated."
}
restore_docker() {
header "Restore: Docker (compose + projects)"
restore_compose
restore_projects
}
restore_etc() {
header "Restore: Host /etc"
etc_restore "ssh" sshd_config ssh_config
etc_restore "fail2ban" jail.local
etc_restore "ufw" ufw.conf user.rules
etc_restore "docker" daemon.json
}
restore_systemd() {
header "Restore: Host /etc/systemd"
systemd_restore "." journald.conf
}
restore_units() {
header "Restore: Host /etc/systemd/system"
units_restore "." restic-backup.service restic-backup.timer
if [[ "${DRY_RUN}" -eq 1 ]]; then
log "DRY: would run: systemctl daemon-reload"
else
systemctl daemon-reload
log "systemd daemon-reload done."
fi
}
restore_host() {
header "Restore: Host (etc + systemd + units)"
restore_etc
restore_systemd
restore_units
}
# ============================================================
# Execution
# ============================================================
case "$TARGET" in
compose) restore_compose ;;
projects) restore_projects ;;
docker) restore_docker ;;
etc) restore_etc ;;
systemd) restore_systemd ;;
units) restore_units ;;
host) restore_host ;;
all) restore_docker; restore_host ;;
*)
error "Invalid target: ${TARGET}"
usage
exit 1
;;
esac
# Local retention for run dirs
perform_retention "${RUNS_BASE_DIR}" "${RUN_RETENTION_DAYS}"
echo ""
finish_run "${RUN_DIR}"
Nadawanie uprawnień
Uprawnienia 0750 pozwalają uruchamiać skrypt tylko rootowi i zaufanej grupie (np. sudo), a jednocześnie blokują dostęp dla pozostałych użytkowników.
sudo chown root:root /srv/config/scripts/repo-restore.sh
sudo chmod 0750 /srv/config/scripts/repo-restore.sh
Uruchomienie odtwarzania
Skrypt uruchamiamy z katalogu skryptów.
Argument --dry-run działa w każdym trybie i wykonuje test odtwarzania bez zapisu zmian.
cd /srv/config/scripts
sudo ./repo-restore.sh --dry-run all
Uruchomienie bez --dry-run wymaga potwierdzenia wpisaniem RESTORE.
cd /srv/config/scripts
sudo ./repo-restore.sh all
Tryb --delete
Argument --delete włącza kasowanie plików w katalogach Dockera, które nie istnieją w repozytorium.
Danger
--delete usuwa lokalne pliki nieobecne w repozytorium, w tym pliki .env w katalogach Docker Compose i projektów.
cd /srv/config/scripts
sudo ./repo-restore.sh --dry-run --delete docker
Tryby pracy
Skrypt przyjmuje jeden argument określający zakres odtwarzania. Tryb domyślny: host.
Odtworzenie Docker Compose
sudo ./repo-restore.sh compose
Odtworzenie projektów Dockera
sudo ./repo-restore.sh projects
Odtworzenie Dockera
sudo ./repo-restore.sh docker
Odtworzenie /etc
sudo ./repo-restore.sh etc
Odtworzenie /etc/systemd
sudo ./repo-restore.sh systemd
Odtworzenie jednostek systemd
sudo ./repo-restore.sh units
Odtworzenie hosta
Odtworzenie konfiguracji hosta:
/etc/etc/systemd/etc/systemd/system
sudo ./repo-restore.sh host
Odtworzenie pełne
sudo ./repo-restore.sh all
Katalog uruchomienia
Każde uruchomienie tworzy katalog w /srv/backups/repo/runs/20YYYYMMDD_HHMM/.
W katalogu zapisywane są:
restore.log– pełny log uruchomieniachanges.txt– zestawienie zmian zrsync --itemize-changesenv-summary.txt– podsumowanie parametrów uruchomienia bez sekretów.SUCCESSlub.FAILED– znacznik wynikuexit-code.txt– kod zakończenia przy błędzie