Preparing the Nodes — Participant Handout
Ansible for Proxmox VE · Module 02
What a cluster needs before it exists
Forming a Proxmox VE cluster fails in predictable ways when the groundwork is missing. This module removes all four causes:
| Requirement | Consequence if missing |
|---|---|
| Usable package sources | apt fails; you cannot install or update anything |
| Mutual name resolution | pvecm and corosync cannot address the other nodes |
| Synchronised clocks | corosync membership becomes unstable |
| Identical base state | problems appear on one node only and are hard to find |
Each requirement becomes one group of tasks in a single playbook.
Playbook structure
A playbook is a list of plays. A play binds a host pattern to a list of tasks.
playbooks/10-node-prep.yml
Conventions worth adopting from the start:
- Give every task a
name:. It is what appears in the output, and it is what--start-at-taskand reviewers rely on. - Use the fully qualified module name (
ansible.builtin.apt, notapt). It removes any doubt about which collection a module comes from. - Keep the playbook free of values. Values belong in variables.
Variables and facts
Your own variables
Files under inventory/group_vars/ are loaded automatically for the group named by the file. inventory/group_vars/all.yml applies to every host.
Facts, gathered from the machine
With gather_facts: true, Ansible collects a few hundred variables per host before the first task runs:
ansible_distribution = "Debian"
ansible_distribution_release = "trixie" # Debian 13 = Proxmox VE 9
ansible_default_ipv4.address = "192.168.0.1"
ansible_hostname = "pve01"
ansible_memtotal_mb = 16384
Your lab nodes run Proxmox VE 9, based on Debian 13 (“trixie”). Using { ansible_distribution_release } instead of hardcoding trixie keeps the playbook working when the next Debian release arrives. Inspect everything a node reports with:
ansible pve01 -m ansible.builtin.setupJob 1: repositories
A fresh Proxmox VE installation has the enterprise repository enabled, which requires a subscription. Without one, every apt update ends in an error. In the lab we disable it and enable the no-subscription repository instead.
- name: Disable the enterprise repositories
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/apt/sources.list.d/pve-enterprise.sources
- /etc/apt/sources.list.d/ceph.sources
notify: Update apt cache
- name: Enable the no-subscription repository
ansible.builtin.deb822_repository:
name: pve-no-subscription
types: [deb]
uris: http://download.proxmox.com/debian/pve
suites: "{{ ansible_distribution_release }}"
components: [pve-no-subscription]
signed_by: /usr/share/keyrings/proxmox-archive-keyring.gpg
notify: Update apt cachedeb822_repository writes the deb822 .sources format that Debian 13 and Proxmox VE 9 use natively.
The no-subscription repository is intended for testing and evaluation. Production systems belong on the enterprise repository with a valid subscription. We use it here because the lab has no subscription, and because it is a realistic first automation task.
The file name of the Proxmox signing key has changed between releases. Check what your nodes actually have before you run the playbook:
ansible pve01 -m ansible.builtin.command -a 'ls /usr/share/keyrings/'Put the correct path into group_vars instead of repeating it in the playbook.
Handlers
A handler is a task that runs only when notified, and only once per play no matter how many tasks notified it.
By default handlers run at the end of the play. Here we need the refreshed cache before installing packages, so we flush them explicitly:
This is the standard pattern whenever a later task depends on a handler’s effect.
Is there an API equivalent?
For this module: mostly no, and that is the point. Package sources, installed packages and /etc/hosts are Debian concerns, and the Proxmox VE API does not model them. The one exception is the subscription status, which is why the enterprise repository exists at all:
TOKEN='PVEAPIToken=root@pam!ansible=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' # pveum user token add root@pam ansible --privsep 0
PVE=https://192.168.0.1:8006/api2/json
# what the node thinks about its subscription
curl -sk -H "Authorization: $TOKEN" "$PVE/nodes/pve01/subscription" | jq
# available updates, as the GUI shows them
curl -sk -H "Authorization: $TOKEN" "$PVE/nodes/pve01/apt/update" | jq '.data[] | {Package, Version}'
# the repository status the GUI warns about
curl -sk -H "Authorization: $TOKEN" "$PVE/nodes/pve01/apt/repositories" | jq '.data.files[].path'API reference: /nodes/{node}/apt and /nodes/{node}/subscription.
Useful for reading state; the actual base configuration happens in files on the node. From module 03 onwards, cluster and workload management moves to the API side.
Job 2: name resolution, verified
Every cluster node has to resolve the others, but that is not the playbook’s job here: DNS and /etc/hosts come from the provisioning, including the real domain of your lab. What the playbook does is confirm the assumption before the cluster depends on it.
Three ideas here return later:
groups['pve']is the inventory as data. The playbook does not contain a list of nodes; it asks the inventory.difference([inventory_hostname])removes the node itself from the loop, so each node only checks its peers.changed_when: falsemarks the task as read-only, which keeps a clean run atchanged=0.
Add a fourth node to inventory/hosts.yml and the check covers it automatically.
/etc/hosts here
It is tempting to generate that file from the inventory, and in a platform you build from scratch it is a reasonable thing to do. In this lab it is not: the provisioning already writes it, together with the node’s real fully qualified domain name. Proxmox VE resolves its own node name through it, so a template that replaces the FQDN with an invented domain changes the node’s identity, quietly and on all three nodes at once.
The rule generalises beyond this workshop: before you automate a file, find out whether something else already owns it.
Jobs 3 and 4: packages and time
- name: Install the admin tools
ansible.builtin.apt:
name: "{{ pve_packages }}"
state: present
- name: Check whether the clock is synchronised
ansible.builtin.command: timedatectl show -p NTPSynchronized --value
register: ntp_state
changed_when: false
check_mode: false
- name: Fail early if time is not in sync
ansible.builtin.assert:
that: ntp_state.stdout == 'yes'
fail_msg: "Time is not synchronised - corosync will be unhappy"changed_when: false tells Ansible that this command only reads. Without it, every run would report a change and your changed=0 proof would be gone.
assert turns an assumption into a check. A playbook that fails here, loudly and early, is much friendlier than a cluster that misbehaves an hour later.
Running it
The third run should end in:
PLAY RECAP
pve01 : ok=7 changed=0 unreachable=0 failed=0 skipped=0
pve02 : ok=7 changed=0 unreachable=0 failed=0 skipped=0
pve03 : ok=7 changed=0 unreachable=0 failed=0 skipped=0
changed=0 is the evidence that your description matches reality. From here on, the playbook is documentation you can execute.
Flags you will use every day
| Flag | Effect |
|---|---|
--check |
dry run: report what would change, change nothing |
--diff |
show the actual difference for file-based modules |
--limit pve02 |
restrict the run to one host or pattern |
--tags repos |
run only tasks carrying that tag |
--start-at-task "…" |
resume at a named task |
-v, -vvv |
more detail; -vvv includes the SSH conversation |
--check cannot predict everything. A task whose input depends on an earlier change may report incorrectly, and command/shell tasks are skipped entirely unless they declare check_mode: false. It still catches the majority of mistakes before they reach all three nodes at once.
Further reading
Playbook concepts from this module
- Intro to playbooks
- Handlers: running operations on change (including
flush_handlers) - Using variables and Discovering variables: facts
- Loops
- Conditionals (
when:,changed_when:,failed_when:) - Validating tasks: check mode and diff mode
- Tags
The modules used here
ansible.builtin.aptansible.builtin.deb822_repositoryansible.builtin.commandansible.builtin.assertansible.builtin.file
Proxmox VE
- Package Repositories, the wiki page that tells you which repository belongs on which release
- Proxmox VE Administration Guide, the complete documentation, also installed on every node under
https://<node>:8006/pve-docs/
Finding things offline
ansible-doc ansible.builtin.deb822_repository
ansible-doc -s ansible.builtin.deb822_repository # task skeleton with all options