Ansible in 30 Minutes

Ansible for Proxmox VE · Module 01

credativ GmbH

Learning goals

  • Explain what a control node, a managed node and an inventory are
  • Run ad-hoc commands against all three Proxmox VE nodes
  • Describe what idempotent means and why it changes how you write things
  • Read the output of an Ansible run without guessing

How Ansible works

Agentless, push-based

  • Nothing is installed on the managed nodes. Ansible connects over SSH.
  • The control node runs ansible-core; the managed nodes only require Python.
  • Playbooks declare the target state, modules execute the necessary changes.

The three things you need

Inventory

Which machines

Module

What to do

Task / Playbook

The plan

Everything else (roles, handlers, variables, templates) is structure on top of these three.

Inventory: your machines, in YAML

all:
  children:
    pve:                      # group: all Proxmox VE nodes
      hosts:
        pve01: {ansible_host: 192.168.0.1}
        pve02: {ansible_host: 192.168.0.2}
        pve03: {ansible_host: 192.168.0.3}
      vars:
        ansible_user: root
    pve_primary:              # group with exactly one member
      hosts: {pve01: null}
    pve_secondary:
      hosts: {pve02: null, pve03: null}

A host may be in several groups. We will use exactly that in module 03.

Check that Ansible reads it the way you meant it:

ansible-inventory --graph

ansible.cfg: stop typing the same flags

[defaults]
inventory          = inventory/hosts.yml
host_key_checking  = False        # lab only, see the warning
interpreter_python = auto_silent

[ssh_connection]
pipelining = True

host_key_checking = False disables SSH host key verification. Acceptable in a throwaway lab, never in production. There you pre-seed known_hosts.

Doing something

Ad-hoc commands

One module, one run, no file:

cd ~/ansible-proxmox          # always. ansible.cfg is read from here
ansible pve -m ping
ansible pve -m ansible.builtin.setup -a 'filter=ansible_distribution*'
ansible pve -m ansible.builtin.command -a 'pveversion'
  • pve is the group from the inventory (all, pve01, pve* also work)
  • -m names the module
  • -a passes its arguments

Your key is on the nodes, so nothing asks for a password. Where it is not, --ask-pass asks once instead of once per task.

Useful for looking. For changing, write a playbook.

Run it from the project directory

Ansible reads ansible.cfg from the current working directory, and that file is what points at your inventory. From anywhere else:

[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available.
[WARNING]: Could not match supplied host pattern, ignoring: pve

Three warnings, no error, nothing ran. Almost always a wrong cd, not a broken inventory.

Reading the output

A module that returns data:

pve01 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.13"
    },
    "changed": false,
    "ping": "pong"
}

SUCCESS means nothing changed, CHANGED means it did. The hosts come back in whatever order they finish — ad-hoc runs them in parallel.

Reading the output

command and shell answer differently: a header line, then raw output.

pve01 | CHANGED | rc=0 >>
pve-manager/9.2.2/b9984c6d90a4bd80 (running kernel: 7.0.2-6-pve)

And when it fails, the return code comes with it:

pve01 | FAILED | rc=2 >>
Error: Corosync config '/etc/pve/corosync.conf' does not exist - is this
node part of a cluster?

Four words to read

Word Meaning
SUCCESS already in the desired state, nothing done
CHANGED Ansible changed something
FAILED the task ran and did not succeed
UNREACHABLE Ansible never got to the host

Idempotency

You describe the target state, not the steps.

ansible pve -m ansible.builtin.file \
  -a 'path=/root/demo state=directory'
  • First run → changed
  • Second run → ok

A playbook that reports only ok still did its job: it checked every statement and found reality already matching.

command is the last resort

- name: This runs every single time
  ansible.builtin.command: pveversion

Ansible cannot know what your command does, so it reports changed even for reading a version number. You saw that in exercise 01.

Look for a module first. Nearly everything you would type on a Proxmox VE node has one, or is reachable through the API.

When there really is no module

Then the guard is yours to write:

- name: Fetch the Debian 13 container template
  ansible.builtin.command: pveam download local debian-13-standard_13.6-1_amd64.tar.zst
  args:
    creates: /var/lib/vz/template/cache/debian-13-standard_13.6-1_amd64.tar.zst
  • creates: — skip the task when that path already exists
  • when: — a condition of your own
  • changed_when: / failed_when: — decide yourself what counts

The documentation

Every module page online has the same content as ansible-doc <module> on your desktop, which is offline and matches the version you are actually running.

Exercise 01 — 10 minutes

  1. Read ansible.cfg and the inventory, check it with ansible-inventory --graph
  2. Reach all three nodes
  3. Aim at one group, then at two hosts — predict the count before you run it
  4. Run the same file command twice

→ Exercise sheet: Ansible in 30 minutes

Exercise 01 · Solutions

1 — Know what you are running

cd ~/ansible-proxmox
ansible-inventory --graph
@all:
  |--@ungrouped:
  |--@pve:
  |  |--pve01
  |  |--pve02
  |  |--pve03
  |--@pve_primary:
  |  |--pve01
  |--@pve_secondary:
  |  |--pve02
  |  |--pve03

pve01 appears twice. Groups are labels, not folders.

2 — Reach the nodes

ansible pve -m ping
ansible pve -m ping --ask-pass      # password123

Three SUCCESS blocks, in whatever order the hosts finish.

Three [WARNING] lines and nothing else means you are in the wrong directory, not that the inventory is broken. Check pwd first.

3 — Aim

ansible pve_primary -m ping     # 1 host
ansible pve_secondary -m ping   # 2 hosts
ansible 'pve0*' -m ping         # 3 hosts

Quote the wildcard, or the shell gets to it first.

Patterns are not shell globs: pve0[23] matches nothing, and an empty match is not an error.

4 — Run the same thing twice

ansible pve -m ansible.builtin.file -a 'path=/root/demo state=directory'   # CHANGED
ansible pve -m ansible.builtin.file -a 'path=/root/demo state=directory'   # SUCCESS

The module stats the path, finds it already right, and reports SUCCESS.

ansible pve -m ansible.builtin.command -a 'mkdir -p /root/demo'   # CHANGED, twice

Same result on the machine, different report. This is why modules beat commands.