pez-infra/ansible/roles/proxmox_ve/tasks/main.yml
Rasmus "Pez" Wejlgaard 0a552b10da Back up the Proxmox VMs to london-b
Neither Proxmox host had a single backup job, so the game servers, the
k3s control planes and the workspace VMs had no backups at all.

copenhagen-a has nowhere local to keep them (`local` is the root LV and
already 82% full), so both hosts now back up to a new pve-backups Samba
share on london-b, each into its own directory. VMIDs overlap between
the two hosts, and retention prunes by VMID, so they can't share one.

- media_stack creates a Samba-only pvebackup user on london-b, the share
  and the per-host directories. The password comes from SOPS and gets
  re-set if the login stops working. smb.conf is now checked with
  testparm before it's deployed.
- proxmox_ve adds a backup-only london-b-backups CIFS storage, pushes the
  credentials again if the storage goes inactive, and fails the deploy
  if it still can't reach london-b.
- Jobs are defined in host_vars. Each job keeps a hash of its settings
  in its comment, so the role only touches jobs that changed and removes
  ones I delete from host_vars. Jobs made by hand are left alone.

london-a backs up the control plane every night. copenhagen-a backs up
the control plane and game servers every night and the workspace VMs
weekly, since every run is a full archive going over the WAN.
2026-09-16 17:52:22 +01:00

190 lines
7.3 KiB
YAML

---
# Configure Proxmox VE apt sources for the no-subscription tier.
# Removes the enterprise repos (require a paid subscription) and enables
# the free no-subscription repo so apt update works without credentials.
# PVE 9 (trixie) uses deb822 .sources files. Clear out the enterprise repos in
# both formats, plus the old one-line no-subscription .list this role used to
# write with a hardcoded bookworm suite (mixing suites on a trixie host).
- name: Remove enterprise and legacy Proxmox apt sources
ansible.builtin.file:
path: "/etc/apt/sources.list.d/{{ item }}"
state: absent
loop:
- pve-enterprise.list
- pve-enterprise.sources
- ceph.list
- ceph.sources
- pve.sources
- pve-no-subscription.list
notify: Update apt cache
- name: Add PVE no-subscription apt source
ansible.builtin.copy:
dest: /etc/apt/sources.list.d/proxmox.sources
content: |
Types: deb
URIs: http://download.proxmox.com/debian/pve
Suites: {{ ansible_facts['distribution_release'] }}
Components: pve-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
owner: root
group: root
mode: "0644"
notify: Update apt cache
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
# Patch is re-applied automatically on each Ansible run, which handles pve-manager package updates
# reverting the file.
- name: Suppress Proxmox subscription nag dialog
ansible.builtin.replace:
path: /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
regexp: "if \\(res === null \\|\\| res === undefined \\|\\| !res \\|\\| res\\.data\\.status\\.toLowerCase\\(\\) !== 'active'\\)"
replace: "if (false)"
notify: Restart pveproxy
- name: Allow Proxmox web UI on Tailscale only
community.general.ufw:
rule: allow
port: "8006"
proto: tcp
interface: tailscale0
direction: in
comment: "Proxmox web UI - Tailscale only"
notify: Reload ufw
# --- Storage ---
# london-b's CIFS share is only mounted for hosts on the same LAN (london-a) —
# it's not worth mounting over the WAN/Tailscale for a Copenhagen host.
- name: Check if hdd CIFS storage is configured
ansible.builtin.command: pvesh get /storage/hdd
register: proxmox_ve_hdd_check
changed_when: false
failed_when: false
when: proxmox_ve_mount_cifs_storage | default(true)
- name: Add hdd CIFS storage (london-b /pve share)
ansible.builtin.command: >-
pvesm add cifs hdd
--server 100.84.65.101
--share pve
--username {{ proxmox_ve_smb_user }}
--password {{ proxmox_ve_smb_password }}
--content images,backup,import,snippets,vztmpl,rootdir,iso
no_log: true
changed_when: true
when:
- proxmox_ve_mount_cifs_storage | default(true)
- proxmox_ve_hdd_check.rc != 0
# --- VM backups (vzdump) ---
# Proxmox has nowhere local to keep backups (copenhagen-a's `local` is the root
# LV, and LVM-thin can't hold archives), so each host backs up to its own
# directory on london-b. Job definitions live in host_vars.
- name: Configure VM backups
when: proxmox_ve_backup_jobs | length > 0
block:
- name: Check if backup storage is configured
ansible.builtin.command: pvesh get /storage/{{ proxmox_ve_backup_storage }}
register: proxmox_ve_backup_storage_check
changed_when: false
failed_when: false
check_mode: false
- name: Add backup storage (london-b pve-backups share)
ansible.builtin.command: >-
pvesm add cifs {{ proxmox_ve_backup_storage }}
--server {{ proxmox_ve_backup_server }}
--share {{ proxmox_ve_backup_share }}
--subdir /{{ inventory_hostname }}
--username {{ proxmox_ve_backup_smb_user }}
--password {{ proxmox_ve_backup_smb_password }}
--content backup
--prune-backups keep-all=1
no_log: true
changed_when: true
when: proxmox_ve_backup_storage_check.rc != 0
- name: Check backup storage is active
ansible.builtin.command: pvesm status --storage {{ proxmox_ve_backup_storage }}
register: proxmox_ve_backup_storage_status
changed_when: false
failed_when: false
check_mode: false
# The add above only runs once, so re-push the credentials whenever the
# storage can't log in — e.g. after the password is rotated in SOPS.
- name: Refresh backup storage credentials
ansible.builtin.command: >-
pvesm set {{ proxmox_ve_backup_storage }}
--username {{ proxmox_ve_backup_smb_user }}
--password {{ proxmox_ve_backup_smb_password }}
no_log: true
changed_when: true
when:
- proxmox_ve_backup_storage_check.rc == 0
- "'active' not in proxmox_ve_backup_storage_status.stdout.split()"
- name: Verify backup storage is active
ansible.builtin.command: pvesm status --storage {{ proxmox_ve_backup_storage }}
register: proxmox_ve_backup_storage_verify
changed_when: false
failed_when: "'active' not in proxmox_ve_backup_storage_verify.stdout.split()"
when: not ansible_check_mode
- name: List existing backup jobs
ansible.builtin.command: pvesh get /cluster/backup --output-format json
register: proxmox_ve_backup_jobs_current
changed_when: false
check_mode: false
# Each managed job carries a hash of its desired settings in its comment,
# so drift is detected without having to normalise the API's output.
- name: Work out backup job changes
ansible.builtin.set_fact:
proxmox_ve_backup_jobs_existing: >-
{{ dict(proxmox_ve_backup_jobs_current.stdout | from_json | map(attribute='id')
| zip(proxmox_ve_backup_jobs_current.stdout | from_json | map(attribute='comment', default=''))) }}
proxmox_ve_backup_comment_prefix: "Managed by Ansible (pez-infra)"
- name: Create or update backup jobs
ansible.builtin.command: >-
pvesh {{ 'set /cluster/backup/' ~ item.id if item.id in proxmox_ve_backup_jobs_existing
else 'create /cluster/backup --id ' ~ item.id }}
--schedule {{ item.schedule | quote }}
--storage {{ proxmox_ve_backup_storage }}
--vmid {{ item.vmids | join(',') }}
--mode snapshot
--compress zstd
--enabled 1
--prune-backups {{ item.prune_backups | quote }}
--comment {{ (proxmox_ve_backup_comment_prefix ~ ' ' ~ proxmox_ve_backup_job_hash) | quote }}
vars:
# Include everything passed to pvesh, so changing a fixed flag here also
# re-applies the jobs.
proxmox_ve_backup_job_hash: >-
{{ (item | combine({'storage': proxmox_ve_backup_storage, 'mode': 'snapshot', 'compress': 'zstd'})
| to_json | hash('sha1'))[:12] }}
loop: "{{ proxmox_ve_backup_jobs }}"
loop_control:
label: "{{ item.id }}"
changed_when: true
when: >-
proxmox_ve_backup_jobs_existing.get(item.id)
!= proxmox_ve_backup_comment_prefix ~ ' ' ~ proxmox_ve_backup_job_hash
- name: Remove backup jobs no longer defined
ansible.builtin.command: pvesh delete /cluster/backup/{{ item.key }}
loop: "{{ proxmox_ve_backup_jobs_existing | dict2items }}"
loop_control:
label: "{{ item.key }}"
changed_when: true
when:
- item.value is string
- item.value.startswith(proxmox_ve_backup_comment_prefix)
- item.key not in proxmox_ve_backup_jobs | map(attribute='id')