backup stuff
This commit is contained in:
parent
01b83a80ec
commit
27e036eccd
16 changed files with 513 additions and 0 deletions
14
ansible/roles/backup_source/templates/backup.service.j2
Normal file
14
ansible/roles/backup_source/templates/backup.service.j2
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[Unit]
|
||||
Description={{ backup_source_description }} backup
|
||||
{% if backup_source_stop_service %}
|
||||
{# systemd rejects a bare name here ("Failed to add dependency ... Invalid
|
||||
argument"), so normalise to a full unit name. #}
|
||||
After={{ backup_source_stop_service if '.' in backup_source_stop_service else backup_source_stop_service ~ '.service' }}
|
||||
{% endif %}
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/{{ backup_source_name }}-backup.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier={{ backup_source_name }}-backup
|
||||
69
ansible/roles/backup_source/templates/backup.sh.j2
Normal file
69
ansible/roles/backup_source/templates/backup.sh.j2
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#!/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 }}"
|
||||
{% if backup_source_stop_service %}
|
||||
SERVICE="{{ backup_source_stop_service }}"
|
||||
{% 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
|
||||
|
||||
{% if backup_source_stop_service %}
|
||||
# --- 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}..."
|
||||
systemctl stop "$SERVICE"
|
||||
trap 'log "Restarting ${SERVICE}..."; systemctl start "${SERVICE}" || true' EXIT
|
||||
{% 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."
|
||||
11
ansible/roles/backup_source/templates/backup.timer.j2
Normal file
11
ansible/roles/backup_source/templates/backup.timer.j2
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[Unit]
|
||||
Description={{ backup_source_description }} backup
|
||||
|
||||
[Timer]
|
||||
OnCalendar={{ backup_source_on_calendar }}
|
||||
# Persistent: a window missed while the host was down runs on next boot. cron on
|
||||
# a laptop had no equivalent, which is how two backups went unnoticed for months.
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Loading…
Add table
Add a link
Reference in a new issue