2026-09-12 16:02:00 +02:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# {{ backup_source_description }} backup — managed by Ansible (roles/backup_source)
|
|
|
|
|
#
|
|
|
|
|
# Dumps to stdout, encrypts with age, writes {{ backup_source_dir }}.
|
|
|
|
|
# The host holds only the age PUBLIC key, so it cannot read its own backups.
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
umask 077
|
|
|
|
|
|
|
|
|
|
BACKUP_DIR="{{ backup_source_dir }}"
|
|
|
|
|
RETENTION_DAYS={{ backup_source_retention_days }}
|
|
|
|
|
RECIPIENT="{{ backup_source_recipient }}"
|
|
|
|
|
SUFFIX="{{ backup_source_artifact_suffix }}"
|
|
|
|
|
NAME="{{ backup_source_name }}"
|
2026-09-12 16:20:42 +02:00
|
|
|
{% if backup_source_stop_service or backup_source_stop_command %}
|
|
|
|
|
STOP_CMD={{ (backup_source_stop_command or ('systemctl stop ' ~ backup_source_stop_service)) | quote }}
|
|
|
|
|
START_CMD={{ (backup_source_start_command or ('systemctl start ' ~ backup_source_stop_service)) | quote }}
|
|
|
|
|
SERVICE="{{ backup_source_stop_service or backup_source_description }}" # label for the log only
|
2026-09-12 16:02:00 +02:00
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
|
|
|
ARTIFACT="${BACKUP_DIR}/${NAME}_${TIMESTAMP}.${SUFFIX}"
|
|
|
|
|
|
|
|
|
|
die() { echo "FATAL: $*" >&2; exit 1; }
|
|
|
|
|
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*"; }
|
|
|
|
|
|
|
|
|
|
# --- Pre-flight ---
|
|
|
|
|
[[ -n "$RECIPIENT" ]] || die "no age recipient configured"
|
|
|
|
|
command -v age >/dev/null || die "age is not installed"
|
|
|
|
|
|
|
|
|
|
# Mode must agree with what the role sets, or each undoes the other every run.
|
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
|
|
|
{% if backup_source_pull_user %}
|
|
|
|
|
chown root:{{ backup_source_pull_user }} "$BACKUP_DIR"
|
|
|
|
|
chmod 750 "$BACKUP_DIR"
|
|
|
|
|
{% else %}
|
|
|
|
|
chmod 700 "$BACKUP_DIR"
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
# A run that died mid-dump leaves a .partial. It is not a backup, and the prune
|
|
|
|
|
# glob below cannot match it (it ends .partial, not .${SUFFIX}), so clear them
|
|
|
|
|
# here or they accumulate forever.
|
|
|
|
|
rm -f "${BACKUP_DIR}/${NAME}_"*.partial
|
|
|
|
|
|
2026-09-12 16:20:42 +02:00
|
|
|
{% if backup_source_stop_service or backup_source_stop_command %}
|
2026-09-12 16:02:00 +02:00
|
|
|
# --- Stop the service, and guarantee it comes back ---
|
|
|
|
|
# The trap is the point: without it a failed dump leaves the service down until
|
|
|
|
|
# the next timer fires. Every hand-written script this replaced had that bug.
|
|
|
|
|
log "Stopping ${SERVICE}..."
|
2026-09-12 16:20:42 +02:00
|
|
|
eval "$STOP_CMD"
|
|
|
|
|
trap 'log "Restarting ${SERVICE}..."; eval "$START_CMD" || true' EXIT
|
2026-09-12 16:02:00 +02:00
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
# --- Dump straight into age; plaintext never touches the disk ---
|
|
|
|
|
log "Writing ${ARTIFACT}..."
|
|
|
|
|
{{ backup_source_dump_command }} | age -r "$RECIPIENT" -o "${ARTIFACT}.partial"
|
|
|
|
|
mv "${ARTIFACT}.partial" "$ARTIFACT"
|
|
|
|
|
{% if backup_source_pull_user %}
|
|
|
|
|
# Readable by the pull account and nobody else. The contents are age-encrypted
|
|
|
|
|
# regardless, so this is depth rather than the actual protection.
|
|
|
|
|
chown root:{{ backup_source_pull_user }} "$ARTIFACT"
|
|
|
|
|
chmod 640 "$ARTIFACT"
|
|
|
|
|
{% else %}
|
|
|
|
|
chmod 600 "$ARTIFACT"
|
|
|
|
|
{% endif %}
|
|
|
|
|
log "Wrote ${ARTIFACT} ($(du -h "$ARTIFACT" | cut -f1))"
|
|
|
|
|
|
|
|
|
|
# --- Prune ---
|
|
|
|
|
log "Pruning local artefacts older than ${RETENTION_DAYS} days..."
|
|
|
|
|
find "$BACKUP_DIR" -maxdepth 1 -type f -name "${NAME}_*.${SUFFIX}" -mtime +"${RETENTION_DAYS}" -delete
|
|
|
|
|
|
|
|
|
|
log "Done."
|