2026-09-12 16:02:00 +02:00
|
|
|
---
|
|
|
|
|
- name: Assert backup_source parameters are sane
|
|
|
|
|
ansible.builtin.assert:
|
|
|
|
|
that:
|
|
|
|
|
- backup_source_name | length > 0
|
|
|
|
|
- backup_source_description | length > 0
|
|
|
|
|
- backup_source_dump_command | length > 0
|
|
|
|
|
- backup_source_recipient | length > 0
|
|
|
|
|
- backup_source_recipient is match('^age1[0-9a-z]{58}$')
|
2026-09-12 16:20:42 +02:00
|
|
|
- not (backup_source_stop_command | length > 0 and backup_source_start_command | length == 0)
|
2026-09-12 16:02:00 +02:00
|
|
|
fail_msg: >-
|
|
|
|
|
backup_source: '{{ backup_source_name | default("<unnamed>") }}' needs a name,
|
|
|
|
|
description, dump command and a valid age recipient (age1... 62 chars).
|
2026-09-12 16:20:42 +02:00
|
|
|
backup_source_stop_command must be paired with backup_source_start_command.
|
2026-09-12 16:02:00 +02:00
|
|
|
quiet: true
|
|
|
|
|
|
|
|
|
|
# Declared here rather than assumed. Stage 1 installed it by hand; this is what
|
|
|
|
|
# makes a rebuilt host get it too.
|
|
|
|
|
- name: Ensure age is installed
|
|
|
|
|
ansible.builtin.apt:
|
|
|
|
|
name: age
|
|
|
|
|
state: present
|
|
|
|
|
update_cache: yes
|
|
|
|
|
cache_valid_time: 3600
|
|
|
|
|
|
|
|
|
|
# The pull account: unprivileged, no sudo, exists only so small-backups-box can
|
|
|
|
|
# read the dump directory. Trust points one way — the box can read backups, and
|
|
|
|
|
# can do nothing else on this host.
|
|
|
|
|
- name: "Ensure the {{ backup_source_pull_user }} account exists"
|
|
|
|
|
ansible.builtin.user:
|
|
|
|
|
name: "{{ backup_source_pull_user }}"
|
|
|
|
|
system: yes
|
|
|
|
|
shell: /bin/sh # rsync-over-ssh needs a shell; nologin breaks it
|
|
|
|
|
home: "/var/lib/{{ backup_source_pull_user }}"
|
|
|
|
|
create_home: yes
|
|
|
|
|
password: '!' # no password login, ever
|
|
|
|
|
when: backup_source_pull_user | length > 0
|
|
|
|
|
|
|
|
|
|
- name: "Authorise the backup box's key for {{ backup_source_pull_user }}"
|
|
|
|
|
ansible.posix.authorized_key:
|
|
|
|
|
user: "{{ backup_source_pull_user }}"
|
|
|
|
|
key: "{{ backup_source_pull_key }}"
|
|
|
|
|
key_options: "restrict" # no pty, no forwarding, no user rc
|
|
|
|
|
exclusive: yes
|
|
|
|
|
state: present
|
|
|
|
|
when: backup_source_pull_user | length > 0
|
|
|
|
|
|
|
|
|
|
# The shared container above the per-service directories. It must be traversable
|
|
|
|
|
# or the pull account cannot reach its own directory. The script's `mkdir -p`
|
|
|
|
|
# runs under `umask 077` and would otherwise create this 0700.
|
|
|
|
|
- name: "Ensure {{ backup_source_dir | dirname }} is traversable"
|
|
|
|
|
ansible.builtin.file:
|
|
|
|
|
path: "{{ backup_source_dir | dirname }}"
|
|
|
|
|
state: directory
|
|
|
|
|
owner: root
|
|
|
|
|
group: root
|
|
|
|
|
mode: '0755'
|
|
|
|
|
|
|
|
|
|
- name: "Ensure {{ backup_source_dir }} exists"
|
|
|
|
|
ansible.builtin.file:
|
|
|
|
|
path: "{{ backup_source_dir }}"
|
|
|
|
|
state: directory
|
|
|
|
|
owner: root
|
|
|
|
|
group: "{{ backup_source_pull_user | default('root', true) }}"
|
|
|
|
|
mode: '0750'
|
|
|
|
|
|
|
|
|
|
- name: "Install the {{ backup_source_name }} backup script"
|
|
|
|
|
ansible.builtin.template:
|
|
|
|
|
src: backup.sh.j2
|
|
|
|
|
dest: "/usr/local/bin/{{ backup_source_name }}-backup.sh"
|
|
|
|
|
owner: root
|
|
|
|
|
group: root
|
|
|
|
|
mode: '0750'
|
|
|
|
|
validate: "bash -n %s"
|
|
|
|
|
|
|
|
|
|
- name: "Install the {{ backup_source_name }}-backup systemd units"
|
|
|
|
|
ansible.builtin.template:
|
|
|
|
|
src: "backup.{{ item }}.j2"
|
|
|
|
|
dest: "/etc/systemd/system/{{ backup_source_name }}-backup.{{ item }}"
|
|
|
|
|
owner: root
|
|
|
|
|
group: root
|
|
|
|
|
mode: '0644'
|
|
|
|
|
loop: [service, timer]
|
|
|
|
|
notify: Reload systemd for backup units
|
|
|
|
|
|
|
|
|
|
- name: "Enable the {{ backup_source_name }}-backup timer"
|
|
|
|
|
ansible.builtin.systemd:
|
|
|
|
|
name: "{{ backup_source_name }}-backup.timer"
|
|
|
|
|
enabled: yes
|
|
|
|
|
state: started
|
|
|
|
|
daemon_reload: yes
|