161 lines
6.4 KiB
YAML
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
|