Skip to content

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/systemd oraz /etc/systemd/system do 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/stderr oraz do pliku sync.log w 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.

repo-common.sh


Tworzenie katalogów

Tworzymy katalogi dla konfiguracji i uruchomień:

shell
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 pliku repo.env


Tworzenie skryptu

Tworzymy plik:

shell
sudo micro /srv/config/scripts/repo-sync.sh

W pliku umieszczamy:

repo-sync.sh
#!/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.

shell
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:

shell
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 uruchomienia
  • changes.txt – zestawienie zmian z rsync --itemize-changes
  • env-summary.txt – podsumowanie parametrów uruchomienia bez sekretów
  • .SUCCESS lub .FAILED – znacznik wyniku
  • exit-code.txt – kod zakończenia przy błędzie