Repo Sync
Konfiguracja synchronizacji repozytorium
Skrypt repo-sync.sh odpowiada za synchronizację wybranych definicji Dockera i konfiguracji hosta do katalogu repozytorium.
Zakres działania skryptu:
- synchronizacja definicji Dockera (Docker Compose oraz projekty) z katalogów źródłowych do repozytorium
- kopiowanie wybranych plików i jednostek z
/etc,/etc/systemdoraz/etc/systemd/systemdo repozytorium - blokada równoległego uruchomienia (mechanizm lock)
- walidacja pliku konfiguracyjnego i wymaganych zmiennych środowiskowych
- retencja katalogów uruchomień
- korekta właściciela i grupy w poddrzewie
docker/w repozytorium
Skrypt działa jako użytkownik root i przeznaczony jest do uruchamiania ręcznego oraz automatycznego.
Założenia konfiguracyjne:
- Skrypt znajduje się w
/srv/config/scripts/ - Plik konfiguracyjny znajduje się w
/srv/backups/repo/repo.env - Logi trafiają na
stdout/stderroraz do plikusync.logw katalogu uruchomienia - Katalog uruchomień znajduje się w
/srv/backups/repo/runs/
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.
Tworzenie katalogów
Tworzymy katalogi dla konfiguracji i uruchomień:
sudo mkdir -p /srv/backups/repo
sudo mkdir -p /srv/backups/repo/runs
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 skryptu
Tworzymy plik:
sudo micro /srv/config/scripts/repo-sync.sh
W pliku umieszczamy:
#!/usr/bin/env bash
# /srv/config/scripts/repo-sync.sh
# Sync selected host config + docker definitions into a repo directory.
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=""
usage() {
cat <<'EOF'
Usage: repo-sync.sh [--dry-run|-n] {compose|projects|docker|etc|systemd|units|host|all}
--dry-run, -n Show what would change, do not write anything.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run|-n) DRY_RUN=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}/sync.log"
CHANGES_FILE="${RUN_DIR}/changes.txt"
: > "${CHANGES_FILE}"
# Log everything to file + stdout (journald)
exec > >(tee -a "${LOG_FILE}") 2>&1
header "Repo Sync: ${TS}"
log "Target: ${TARGET} (dry_run=${DRY_RUN})"
# --- Preflight ---
check_root
acquire_lock "repo-sync"
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 /"
[[ "${REPO_ROOT}" != "/srv" && "${REPO_ROOT}" != "/srv/" ]] || critical "SAFETY STOP: REPO_ROOT must not be /srv"
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-sync" \
"target=${TARGET}" \
"dry_run=${DRY_RUN}" \
"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)"
# ============================================================
# Helpers
# ============================================================
# Base rsync args for sync operations (host -> repo)
rsync_sync() {
local src="$1"
local dst="$2"
require_nonempty "$src" "src"
require_nonempty "$dst" "dst"
local -a args=(
rsync
-a
--delete
--delete-delay
--safe-links
--numeric-ids
--itemize-changes
--info=FLIST2,STATS2
"${RSYNC_EXCLUDES[@]}"
)
if [[ "${DRY_RUN}" -eq 1 ]]; then
args+=(--dry-run)
fi
# Ensure trailing slashes for directory sync semantics
[[ "$src" == */ ]] || src="${src}/"
[[ "$dst" == */ ]] || dst="${dst}/"
"${args[@]}" "$src" "$dst" 2>&1 | tee -a "${CHANGES_FILE}"
}
copy_files() {
local src_dir="$1"
local dst_rel_path="$2"
shift 2
local files=("$@")
require_nonempty "$src_dir" "src_dir"
require_nonempty "$dst_rel_path" "dst_rel_path"
local full_dst="${REPO_ROOT}/${dst_rel_path}"
require_under "${REPO_ROOT}" "${full_dst}"
mkdir -p "$full_dst"
for f in "${files[@]}"; do
local src="${src_dir}/${f}"
local dst="${full_dst}/${f}"
if [[ -f "$src" ]]; then
if [[ "${DRY_RUN}" -eq 1 ]]; then
echo " DRY: would copy: ${dst_rel_path}/${f}"
else
cp -a -- "$src" "$dst"
echo " + Copied: ${dst_rel_path}/${f}"
fi
else
echo -e " ${YELLOW}! Skipped (missing): ${src}${NC}"
fi
done
}
# Only chown repo docker subtree (compromise)
fix_permissions_docker() {
header "Fixing Permissions"
local docker_dir="${REPO_ROOT}/docker"
if [[ ! -d "${docker_dir}" ]]; then
warn "Repo docker dir missing: ${docker_dir} (skipping chown)"
return
fi
if [[ "${DRY_RUN}" -eq 1 ]]; then
log "DRY: would chown -R ${REPO_USER}:${REPO_GROUP} ${docker_dir}"
return
fi
log "Chown repo docker subtree: ${docker_dir} -> ${REPO_USER}:${REPO_GROUP}"
chown -R "${REPO_USER}:${REPO_GROUP}" "${docker_dir}"
log "Done."
}
# Wrappers: /etc/<dir> -> repo/host/etc/<dir>
etc_copy() {
local rel_dir="$1"
shift
local src_dir="/etc"
local dst_rel="host/etc"
if [[ "$rel_dir" != "." ]]; then
src_dir="/etc/${rel_dir}"
dst_rel="host/etc/${rel_dir}"
fi
copy_files "$src_dir" "$dst_rel" "$@"
}
# Wrappers: /etc/systemd/<dir> -> repo/host/etc/systemd/<dir>
systemd_copy() {
local rel_dir="$1"
shift
local src_dir="/etc/systemd"
local dst_rel="host/etc/systemd"
if [[ "$rel_dir" != "." ]]; then
src_dir="/etc/systemd/${rel_dir}"
dst_rel="host/etc/systemd/${rel_dir}"
fi
copy_files "$src_dir" "$dst_rel" "$@"
}
# Wrappers: /etc/systemd/system/<dir> -> repo/host/etc/systemd/system/<dir>
units_copy() {
local rel_dir="$1"
shift
local src_dir="/etc/systemd/system"
local dst_rel="host/etc/systemd/system"
if [[ "$rel_dir" != "." ]]; then
src_dir="/etc/systemd/system/${rel_dir}"
dst_rel="host/etc/systemd/system/${rel_dir}"
fi
copy_files "$src_dir" "$dst_rel" "$@"
}
# ============================================================
# Sync modules
# ============================================================
sync_compose() {
header "Sync: Docker Compose"
local dst="${REPO_ROOT}/docker/compose"
mkdir -p "$dst"
if [[ ! -d "${SOURCE_COMPOSE}" ]]; then
warn "SOURCE_COMPOSE not found: ${SOURCE_COMPOSE} (skipping)"
return
fi
log "Rsync: ${SOURCE_COMPOSE} -> ${dst}"
rsync_sync "${SOURCE_COMPOSE}" "${dst}"
}
sync_projects() {
header "Sync: Docker Projects"
local dst="${REPO_ROOT}/docker/projects"
mkdir -p "$dst"
if [[ ! -d "${SOURCE_PROJECTS}" ]]; then
warn "SOURCE_PROJECTS not found: ${SOURCE_PROJECTS} (skipping)"
return
fi
log "Rsync: ${SOURCE_PROJECTS} -> ${dst}"
rsync_sync "${SOURCE_PROJECTS}" "${dst}"
}
sync_docker() {
header "Sync: Docker (compose + projects)"
sync_compose
sync_projects
}
sync_etc() {
header "Sync: Host /etc"
etc_copy "ssh" sshd_config ssh_config
etc_copy "fail2ban" jail.local
etc_copy "ufw" ufw.conf user.rules
etc_copy "docker" daemon.json
}
sync_systemd() {
header "Sync: Host /etc/systemd"
systemd_copy "." journald.conf
}
sync_units() {
header "Sync: Host /etc/systemd/system"
units_copy "." restic-backup.service restic-backup.timer
}
sync_host() {
header "Sync: Host (etc + systemd + units)"
sync_etc
sync_systemd
sync_units
}
# ============================================================
# Execution
# ============================================================
case "$TARGET" in
compose) sync_compose; fix_permissions_docker ;;
projects) sync_projects; fix_permissions_docker ;;
docker) sync_docker; fix_permissions_docker ;;
etc) sync_etc ;;
systemd) sync_systemd ;;
units) sync_units ;;
host) sync_host ;;
all) sync_docker; sync_host; fix_permissions_docker ;;
*)
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-sync.sh
sudo chmod 0750 /srv/config/scripts/repo-sync.sh
Uruchomienie synchronizacji
Skrypt uruchamiamy z katalogu skryptów.
cd /srv/config/scripts
sudo ./repo-sync.sh all
Tryby pracy
Skrypt przyjmuje jeden argument określający zakres synchronizacji.
Synchronizacja Docker Compose
Synchronizacja definicji Docker Compose:
sudo ./repo-sync.sh compose
Synchronizacja projektów Dockera
Synchronizacja katalogu projektów Dockera:
sudo ./repo-sync.sh projects
Synchronizacja Dockera
Synchronizacja Docker Compose oraz projektów:
sudo ./repo-sync.sh docker
Synchronizacja /etc
Kopiowanie wybranych plików z /etc do repozytorium:
sudo ./repo-sync.sh etc
Synchronizacja /etc/systemd
Kopiowanie wybranych plików z /etc/systemd do repozytorium:
sudo ./repo-sync.sh systemd
Synchronizacja jednostek systemd
Kopiowanie wybranych jednostek z /etc/systemd/system do repozytorium:
sudo ./repo-sync.sh units
Synchronizacja hosta
Kopiowanie konfiguracji hosta:
/etc/etc/systemd/etc/systemd/system
sudo ./repo-sync.sh host
Synchronizacja pełna
Synchronizacja Dockera oraz hosta:
sudo ./repo-sync.sh all
Argument dry-run
Argument --dry-run uruchamia synchronizację bez zapisu zmian i działa w każdym trybie.
sudo ./repo-sync.sh --dry-run all
Katalog uruchomienia
Każde uruchomienie tworzy katalog w /srv/backups/repo/runs/20YYYYMMDD_HHMM/.
W katalogu zapisywane są:
sync.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