Role: proxmox¶
Proxmox VE host baseline — everything the installer does not set up for us. APT repos, kernel pin, nouveau blacklist, sysctl migration, and storage registration.
Hosts: proxfold
Tasks¶
| Task | Tag |
|---|---|
Render pve-no-subscription.sources (deb822) |
proxmox, repos |
Remove legacy .list-format no-subscription files |
proxmox, repos |
Disable pve-enterprise repo (deb822 + legacy .list) |
proxmox, repos |
Disable ceph enterprise repo — legacy .list (absent) and deb822 .sources (Enabled: false injected) |
proxmox, repos |
Pin kernel via proxmox-boot-tool kernel pin |
proxmox, kernel |
Blacklist nouveau in /etc/modprobe.d/ |
proxmox, nouveau |
Migrate custom /etc/sysctl.conf values to /etc/sysctl.d/99-homelab.conf |
proxmox, sysctl |
Import ZFS pool (stash) with -f if not already imported |
proxmox, storage |
Register nasbackup CIFS storage via pvesm add cifs |
proxmox, storage |
Include pbs_client.yml (PBS storage + backup job) — gated on pbs_client is defined |
proxmox, storage, pbs |
Include host_backup.yml (file-level backup of host config to PBS) — gated on pbs_host_backup is defined AND vault_pbs_host_token_secret is defined |
proxmox, pbs, host_backup |
Include notifications.yml (PVE 9 webhook endpoint + match-all matcher) — gated on pve_discord_endpoint_name is defined |
proxmox, notifications |
Key variables¶
| Variable | Source | Value on proxfold |
|---|---|---|
proxmox_repo_suite |
group_vars → host_vars | trixie |
proxmox_repo_components |
group_vars → host_vars | pve-no-subscription |
proxmox_enterprise_disabled |
defaults | true |
proxmox_ceph_repo_disabled |
defaults | true |
proxmox_kernel_pin |
proxfold host_vars | 6.14.11-6-pve |
proxmox_nouveau_blacklist |
proxfold host_vars | true |
nasbackup |
proxfold host_vars | {id: nasbackup, server: 192.168.1.253, share: backup, username: admin, smbversion: "2.0"} |
vault_nasbackup_password |
vault | CIFS password (no_log on the task) |
pbs_client |
proxfold host_vars | {id: nas-primary, server: 192.168.1.246, datastore: nas-primary} |
pbs_backup_job |
proxfold host_vars | {id: pbs-daily, schedule: "02:00", storage: nas-primary, all: 1, mode: snapshot, ...} |
vault_pbs_token_id / vault_pbs_token_secret / vault_pbs_fingerprint |
vault | PBS API token + cert fingerprint (no_log on the pvesm add task) |
pbs_host_backup |
proxfold host_vars | {repository, fingerprint, token_secret, namespace: "host/proxfold", schedule: "02:30", paths: [...]} — see Phase 5E |
vault_pbs_host_token_secret |
vault | PBS host-backup token secret (separate user from pbs_client); gates the role include |
pve_discord_endpoint_name |
proxfold host_vars | discord-ops (webhook endpoint name) |
pve_discord_matcher_name |
proxfold host_vars | ops-all (match-all matcher, routes to endpoint) |
vault_discord_webhook_homelab_ops |
vault | Discord webhook URL, shared with Beszel + ZED |
zfs_pool |
group_vars | stash (reused from the zfs role) |
Templates¶
| Template | Deploys to |
|---|---|
pve-no-subscription.sources.j2 |
/etc/apt/sources.list.d/pve-no-subscription.sources |
blacklist-nouveau.conf.j2 |
/etc/modprobe.d/blacklist-nouveau.conf |
pbs-host-backup.env.j2 |
/etc/pbs-host-backup.env (mode 0600, no_log) |
pbs-host-backup.sh.j2 |
/usr/local/sbin/pbs-host-backup (mode 0750) |
pbs-host-backup.service.j2 |
/etc/systemd/system/pbs-host-backup.service |
pbs-host-backup.timer.j2 |
/etc/systemd/system/pbs-host-backup.timer |
Handlers¶
apt update— runs after any repo file changesreload sysctl— runssysctl --systemafter migrating values to/etc/sysctl.d/rebuild initramfs— shared with the zfs role; fires after nouveau blacklist changes
Why each piece exists¶
deb822 repos. PVE 9 shipped with a mix of old .list and new deb822 repo files. The role standardises on deb822 and cleans up legacy files so drift detection stays quiet.
PVE 9's fresh install also places the enterprise ceph repo in deb822 form at /etc/apt/sources.list.d/ceph.sources. The role disables it by injecting Enabled: false at BOF rather than removing the file, because apt install of any ceph meta-package recreates it.
Kernel pin. The Nvidia 550 DKMS build fails to compile on kernel 6.17. Pinning to 6.14.11-6-pve keeps the GPU working until the driver catches up. See PVE 9 Upgrade — Lessons for the incident that prompted this.
sysctl migration. PVE 9 no longer reads /etc/sysctl.conf. The migration task copies any non-comment lines from the old file into /etc/sysctl.d/99-homelab.conf so custom values survive the upgrade. Idempotent — if /etc/sysctl.conf is empty the task is a no-op.
Storage. ZFS pool import handles the case where stash was exported (post-rebuild, manual admin actions). The import uses -f because a fresh PVE install has a new hostid that won't match the one the pool last saw — plain zpool import refuses in that case. Safe in this homelab (one host per pool, no dual-owner scenario). The CIFS registration writes the password from vault and runs with no_log: true.
PBS client (pbs_client.yml). Loaded via include_tasks when pbs_client is defined on the host. Registers the PBS datastore as PVE storage (pvesm add pbs) using the API token from vault, then creates the vzdump backup job (pvesh create /cluster/backup). Both tasks are idempotent — storage check via pvesm status -storage, job check via pvesh get /cluster/backup. Secrets are no_log. The subfile layout (rather than inlining) keeps main.yml readable and lets a future non-PBS PVE host skip this block cleanly by not defining pbs_client.
Host-level file backup (host_backup.yml). Loaded when pbs_host_backup is defined AND vault_pbs_host_token_secret is defined. Renders a credentials env file (mode 0600), a wrapper script invoking proxmox-backup-client backup against the host's host/<hostname> namespace with --crypt-mode none, plus a systemd oneshot service + daily timer at 02:30. The dual-gate is deliberate: the host_vars block can land before the PBS-side bootstrap completes, and the role stays inert (drift --check --diff continues to pass) until the vault entry is appended. PBS-side bootstrap (separate user, token, namespace, ACL, namespace-scoped prune-job) is one-shot manual — see the backup-restore runbook. Closes the gap left by pbs_client.yml, which only captures guest images via vzdump, never the host's own /etc, /root, or /var/lib/pve-cluster.
PVE 9 notifications (notifications.yml). Loaded when pve_discord_endpoint_name is defined. Creates a webhook endpoint pointing at the shared #homelab-ops Discord channel plus a match-all matcher routing all events through it. Default mail-to-root matcher stays in place; events fan out to both paths. Body template is stored in roles/proxmox/files/discord-notification-body.json (uses PVE's Handlebars templating with {{ escape ... }} and severity-based color conditionals) and loaded via lookup('file', ...) so Ansible's Jinja engine doesn't clash with the Handlebars syntax. Two gotchas captured during execution:
pveshrequires--body,--header value, and--secret valueas base64-encoded strings (perpvesh usage .../webhookschema output); raw values silently store but fail at delivery with "could not decode base64 value". The role passes these through| b64encode.- PVE 9's template language has no truncation helper (only
url-encode,escape,jsonper the official docs). Embedding{{ message }}works for synthetic test events (short) but real backup summaries overflow Discord's 4096-char embed description limit and the webhook returns HTTP 400. The body template uses title + severity-colored embed + hostname footer only — 256-char title always fits, full task log remains one click away in the PBS UI.
Idempotency via pvesh get /cluster/notifications/endpoints/webhook and .../matchers.
The role also disables the built-in default-matcher (which targets mail-to-root with mode=all). Without disabling, every PVE event hits both default-matcher and ops-all. The default-matcher writes to /var/mail/root, which proxmox-mail-forward (PVE's legacy-mail-to-new-notifications bridge, installed by default) intercepts and re-fires through the notification system — ops-all matches the re-fire too, producing a duplicate Discord delivery on every event. Disabling default-matcher breaks the loop. System mails (cron, smartd) still reach Discord via proxmox-mail-forward → ops-all → webhook as a single fire, so legacy mail sources aren't lost.
Related¶
- proxfold — Proxmox host page
- PVE 9 Upgrade runbook
- Proxfold rebuild runbook
- zfs role — shares the
rebuild initramfshandler - nvidia role — depends on the nouveau blacklist