homelab/build/config/ansible/roles/pve_backup/tasks/main.yml
Russell Seymour 1250c9cef6
Some checks are pending
deploy / deploy (push) Waiting to run
Initial checkin of code for managing homelab
2026-08-24 20:52:36 +01:00

161 lines
6.4 KiB
YAML

---
# Configures backups on a Proxmox node: a storage on the NAS to write archives
# to, and a scheduled vzdump job that fills it.
#
# This is the off-box copy for every Proxmox guest. Guests deliberately keep
# all their state on their own rootfs rather than bind-mounting anything in,
# because vzdump excludes bind mounts — see src/forgejo/terraform/README.md.
# Where an app's data lives outside the container (the shared Postgres), the
# app dumps it into its own filesystem on a timer that runs before the window
# below, so one archive is one restore point.
#
# Driven through `pvesm`/`pvesh` rather than modules: as of writing,
# `community.proxmox` has no storage or backup-job module. Swap them in if that
# changes — the shape here is create-if-absent, update-if-drifted, which is
# what a module would do anyway.
#
# UNVERIFIED — this has not been run against a real node. `pvesm add` and
# `pvesh create /cluster/backup` accept slightly different options across PVE
# versions; check yours with `pvesm help add`, `pvesh help create
# /cluster/backup`, and a `--check` run before trusting it.
- name: Check that the storage backend is configured
ansible.builtin.assert:
that:
- pve_backup_storage_type in ['nfs', 'pbs']
- pve_backup_storage_type != 'nfs' or (pve_backup_nfs_server | length > 0
and pve_backup_nfs_export | length > 0)
- pve_backup_storage_type != 'pbs' or (pve_backup_pbs_server | length > 0
and pve_backup_pbs_datastore | length > 0)
- pve_backup_all or (pve_backup_vmids | length > 0)
fail_msg: >-
pve_backup is missing required settings for
pve_backup_storage_type={{ pve_backup_storage_type }}. Set them in
inventory/host_vars/{{ inventory_hostname }}.yml.
quiet: true
- name: Build the prune-backups option string
ansible.builtin.set_fact:
pve_backup_prune_string: >-
{% for key, value in pve_backup_prune.items() %}{{ key }}={{ value }}{% if not loop.last %},{% endif %}{% endfor %}
# --- Storage -------------------------------------------------------------
- name: Read the configured storages
ansible.builtin.command:
argv: [pvesh, get, /storage, --output-format, json]
register: pve_storages
changed_when: false
check_mode: false
- name: Add the backup storage (NFS)
ansible.builtin.command:
argv:
- pvesm
- add
- nfs
- "{{ pve_backup_storage }}"
- --server
- "{{ pve_backup_nfs_server }}"
- --export
- "{{ pve_backup_nfs_export }}"
- --options
- "{{ pve_backup_nfs_options }}"
- --content
- backup
- --prune-backups
- "{{ pve_backup_prune_string }}"
when:
- pve_backup_storage_type == 'nfs'
- pve_backup_storage not in (pve_storages.stdout | from_json | map(attribute='storage') | list)
- name: Add the backup storage (PBS)
# The password goes on a command line, which is visible in the node's process
# list for the moment the command runs. `pvesm` has no stdin form, so the
# alternative is configuring PBS by hand — acceptable for a one-time create
# that only fires when the storage is absent.
ansible.builtin.command:
argv:
- pvesm
- add
- pbs
- "{{ pve_backup_storage }}"
- --server
- "{{ pve_backup_pbs_server }}"
- --datastore
- "{{ pve_backup_pbs_datastore }}"
- --username
- "{{ pve_backup_pbs_username }}"
- --password
- "{{ pve_backup_pbs_password }}"
- --fingerprint
- "{{ pve_backup_pbs_fingerprint }}"
- --content
- backup
- --prune-backups
- "{{ pve_backup_prune_string }}"
when:
- pve_backup_storage_type == 'pbs'
- pve_backup_storage not in (pve_storages.stdout | from_json | map(attribute='storage') | list)
no_log: true
# --- Backup job ----------------------------------------------------------
- name: Read the configured backup jobs
ansible.builtin.command:
argv: [pvesh, get, /cluster/backup, --output-format, json]
register: pve_backup_jobs
changed_when: false
check_mode: false
# Proxmox assigns job IDs itself, so there is nothing stable to key on except
# a comment we set. Without this the role would add a duplicate job on every
# run.
- name: Find a job this role already created
ansible.builtin.set_fact:
pve_backup_existing: >-
{{ (pve_backup_jobs.stdout | from_json)
| selectattr('comment', 'defined')
| selectattr('comment', 'equalto', pve_backup_comment)
| list | first | default({}) }}
- name: Build the vzdump job arguments
ansible.builtin.set_fact:
pve_backup_job_args: >-
{{ ['--schedule', pve_backup_schedule,
'--storage', pve_backup_storage,
'--mode', pve_backup_mode,
'--compress', pve_backup_compress,
'--comment', pve_backup_comment,
'--enabled', ('1' if pve_backup_enabled else '0')]
+ (['--all', '1'] if pve_backup_all
else ['--all', '0', '--vmid', pve_backup_vmids | join(',')])
+ (['--mailto', pve_backup_mailto] if pve_backup_mailto | length > 0 else []) }}
- name: Create the vzdump job
ansible.builtin.command:
argv: "{{ ['pvesh', 'create', '/cluster/backup'] + pve_backup_job_args }}"
when: pve_backup_existing | length == 0
# Compared field by field rather than blindly re-applying, so the role reports
# a change only when there is one. The VMID list is compared as the
# comma-joined string Proxmox stores, which makes it order-sensitive — reorder
# pve_backup_vmids and you get one no-op update.
- name: Work out whether the existing job still matches
ansible.builtin.set_fact:
pve_backup_drifted: >-
{{ pve_backup_existing.schedule | default('') != pve_backup_schedule
or pve_backup_existing.storage | default('') != pve_backup_storage
or pve_backup_existing.mode | default('') != pve_backup_mode
or (pve_backup_existing.enabled | default(1) | int == 1) != pve_backup_enabled
or (pve_backup_existing.all | default(0) | int == 1) != pve_backup_all
or (not pve_backup_all
and pve_backup_existing.vmid | default('') != pve_backup_vmids | join(',')) }}
when: pve_backup_existing | length > 0
- name: Update the vzdump job if its settings drifted
ansible.builtin.command:
argv: "{{ ['pvesh', 'set', '/cluster/backup/' ~ pve_backup_existing.id] + pve_backup_job_args }}"
when:
- pve_backup_existing | length > 0
- pve_backup_drifted | bool