L’hypothèse qu’Ansible a d’ordinaire le droit de faire

Presque chaque module Ansible que vous avez utilisé marche ainsi : Ansible se connecte à l’hôte nommé dans l’inventaire, y copie un petit programme Python, l’exécute, et relit le résultat. L’hôte est la chose modifiée et la chose qui fait le travail.

Créer une machine virtuelle casse cela de la façon la plus élémentaire possible. L’hôte que vous construisez n’existe pas. Il n’a pas d’IP, pas de démon SSH, pas de Python, et pas de système d’exploitation. Il n’y a rien à quoi se connecter.

community.proxmox n’est donc pas un agent de configuration. C’est un client d’API qui se trouve être livré comme une collection Ansible. À ce titre, tout dans ce billet découle de ce seul fait — où les tâches tournent, comment vous y faites entrer des identifiants, pourquoi relancer ne fait pas ce que vous attendez, et pourquoi --check ne vous dit pas la vérité.

Les exemples ici sont réduits à partir d’un playbook qui construit des VM Windows et Linux depuis des enregistrements NetBox : proxmox-create-vms.yml. J’ai rendu génériques les noms de nœud, de stockage et de pont pour la lisibilité — la vraie chose est dans ce dépôt.

Tout ce que j’affirme sur le comportement des modules ci-dessous a été vérifié contre community.proxmox 1.6.0, qui est la version que j’ai installée :

$ ansible-galaxy collection list community.proxmox
# /home/damien/.ansible/collections/ansible_collections
Collection        Version
----------------- -------
community.proxmox 1.6.0

D’abord : la collection a déménagé

Si vous lisez un playbook plus ancien ou une réponse plus ancienne, les modules s’appelaient community.general.proxmox_kvm. Ils vivent désormais dans une collection dédiée, et c’est là que le développement se fait. La version 1.6.0 livre 47 modules, couvrant Ceph, SDN, pare-feu, règles HA et jointure de cluster, dont aucun n’existait à l’ère community.general.

ansible-galaxy collection install community.proxmox
pip install 'proxmoxer>=2.0' requests

La dépendance Python n’est pas optionnelle et pas embarquée : la collection déclare requirements: ["proxmoxer >= 2.0", "requests"], et ceux-là doivent être installés là où le module s’exécute réellement — ce qui, comme l’explique la section suivante, n’est pas le nœud Proxmox.

requirements.yml, si vous préférez l’épingler :

---
collections:
  - name: community.proxmox
    version: ">=1.6.0"

La collection est testée contre ansible-core 2.17 à 2.20. Renommer vos tâches community.general.proxmox_* en community.proxmox.proxmox_* est l’essentiel de la migration.

Chaque tâche Proxmox tourne sur localhost

Deux lignes en haut du play font le gros du travail, et les deux ont l’air de désactiver quelque chose d’utile :

- name: Create Proxmox virtual machines
  hosts: "{{ target_hosts | default('cluster_pve:&status_planned') }}"
  gather_facts: false
  serial: 1

gather_facts: false n’est pas une optimisation. La collecte de faits se connecte à l’hôte d’inventaire, et l’hôte d’inventaire est une VM qui n’a pas encore été construite. Laissez-la active et le play échoue avant la première tâche.

Puis chaque tâche Proxmox porte delegate_to: localhost :

- name: Create Proxmox VM
  delegate_to: localhost
  register: created_vm
  community.proxmox.proxmox_kvm:
    api_user: "{{ proxmox_user }}"
    api_password: "{{ proxmox_password }}"
    api_host: "{{ proxmox_api_ip }}"
    name: "{{ inventory_hostname }}"
    node: "{{ proxmox_api_host }}"
    ...

L’hôte d’inventaire n’est plus qu’un nom et un sac de variables. inventory_hostname devient le nom de la VM ; ses variables décrivent la machine que vous voulez. Rien ne s’y connecte. La tâche tourne sur le nœud de contrôle, qui ouvre une session HTTPS vers api_host et poste une définition de VM.

Vous verrez aussi cela écrit local_action:, qui est la syntaxe plus ancienne pour la même chose. Le playbook de démantèlement dans ce dépôt l’utilise partout. Ils sont équivalents. delegate_to est l’orthographe actuelle.

La trappe d’échappement pointe vers le nœud

Certaines choses doivent vraiment se passer sur un hôte Proxmox, et ces tâches délèguent ailleurs entièrement :

- name: Fail if no ISO file exists for the OS
  delegate_to: "{{ proxmox_api_host }}"
  ansible.builtin.stat:
    path: "{{ iso | replace('isos:', '/mnt/pve/isos/template/') }}"
  register: iso_file
  failed_when: not iso_file.stat.exists

C’est une vraie connexion SSH à un vrai nœud, vérifiant un vrai chemin sur du stockage partagé, parce que l’API acceptera volontiers une référence d’ISO qui ne se résout pas en fichier et vous préférez le découvrir maintenant plutôt qu’au démarrage. Notez la chirurgie de chaîne traduisant une référence de stockage PVE (isos:iso/debian.iso) en chemin de système de fichiers. L’abstraction de stockage n’est pas disponible pour stat.

Un seul play a donc des tâches qui s’exécutent en trois endroits différents, et les mélanger est la façon la plus courante dont ces playbooks échouent :

Où chaque tâche d'un playbook de construction Proxmox s'exécute réellementNœud de contrôle Ansibledelegate_to: localhostproxmox_kvmproxmox_vm_infoproxmox_diskproxmox_access_aclchacun d'eux est unclient HTTPS, pas un agentproxmoxer ≥ 2.0 + requestsinstallés ici, pas sur le nœudgather_facts: falseNœud Proxmox — pve1pvedaemon, REST APIsur le port 8006qm, /etc/pve, storagela définition de la VM atterrit icila VM que vous créezno IP · no SSH · no Pythonpas de système d'exploitationdans l'inventaire ce n'est qu'un nom et un sac de variablesAPISSHqm setstatrien à quoi se connecterpas avant un play ultérieur
Trois contextes d’exécution dans un play. Les modules Proxmox ne touchent jamais le nœud ni l’invité — ce sont des clients HTTPS qui tournent à côté du playbook. La trappe d’échappement qm est la seule partie qui a besoin de SSH vers un hyperviseur.

Identifiants, et un défaut sur le point de changer

Les options d’authentification sont partagées par chaque module de la collection à travers un fragment de documentation, donc elles sont les mêmes partout : api_host, api_user, puis soit api_password, soit la paire api_token_id / api_token_secret. Toutes retombent sur des variables d’environnement — PROXMOX_HOST, PROXMOX_USER, PROXMOX_PASSWORD, PROXMOX_TOKEN_ID, PROXMOX_TOKEN_SECRET, PROXMOX_VALIDATE_CERTS — ce qui est la façon la plus propre de garder les secrets entièrement hors du play.

Un jeton d’API est le meilleur défaut. Il est cadré, il est révocable sans changer le mot de passe d’un humain, et on peut lui donner exactement les privilèges dont le playbook a besoin plutôt que ceux qu’une personne a par hasard :

- name: Create Proxmox VM
  delegate_to: localhost
  community.proxmox.proxmox_kvm:
    api_host: "{{ proxmox_api_ip }}"
    api_user: ansible@pve
    api_token_id: automation
    api_token_secret: "{{ proxmox_token_secret }}"
    validate_certs: true
    ca_path: /etc/ssl/certs/pve-cluster-ca.pem

Réglez validate_certs explicitement, aujourd’hui. La propre documentation de la collection le dit clairement :

Currently defaults to false and changes default to true with community.proxmox 2.0.0.

Ce qui veut dire qu’un playbook qui ne le mentionne jamais ne valide pas TLS en ce moment, et se mettra à valider — et donc à échouer contre le certificat auto-signé que chaque installation Proxmox fraîche livre — dès que quelqu’un lancera --upgrade. Mieux vaut prendre cette décision exprès que de la voir atterrir au milieu d’un montage. Si vous gardez le certificat auto-signé, dites validate_certs: false et assumez le constat ; si vous avez une vraie chaîne, pointez ca_path dessus. Dans un cas comme dans l’autre, c’est écrit.

Le minimum viable de création

Réduisez la tâche de production à ce qui définit réellement une machine et c’est lisible :

- name: Create Proxmox VM
  delegate_to: localhost
  register: created_vm
  community.proxmox.proxmox_kvm:
    api_host: "{{ proxmox_api_ip }}"
    api_user: ansible@pve
    api_token_id: automation
    api_token_secret: "{{ proxmox_token_secret }}"
    validate_certs: true

    node: pve1
    name: "{{ inventory_hostname }}"
    cores: "{{ vcpus | int }}"
    memory: "{{ memory }}"
    machine: q35
    bios: ovmf
    ostype: l26
    scsihw: virtio-scsi-single
    scsi:
      scsi0: "vmdata:32,format=qcow2,discard=on,ssd=1"
    sata:
      sata0: "isos:iso/debian-13-netinst.iso,media=cdrom"
    net:
      net0: "virtio,bridge=vmbr0"
    efidisk0:
      storage: vmdata
      format: raw
      efitype: 4m
      pre_enrolled_keys: true
    boot: "order=scsi0;sata0"
    agent: "enabled=1,fstrim_cloned_disks=1"
    onboot: true
    tags:
      - production

Quelques choses sur cette forme valent d’être connues avant d’écrire la vôtre.

Les options de périphérique sont de la syntaxe PVE dans du YAML. scsi, sata, net, virtio, ide sont tous typés dict, clés scsi0, net0 et ainsi de suite, et les valeurs sont les chaînes d’options séparées par des virgules tout droit sorties de man qm — <storage>:<size>,option=value pour un disque, [model=]<enum>,option=value pour une NIC. Le module ne les modélise pas ; il les transmet. Quand quelque chose est rejeté, la réponse est dans la référence des options PVE, pas dans la doc Ansible.

Vous les verrez aussi écrites en chaîne JSON plutôt qu’en mappage YAML :

    net: '{"net0":"virtio,bridge={{ vlan_bridge }}"}'

Les deux marchent. Ansible coerce la chaîne pour un paramètre typé dict. La forme JSON existe parce qu’il est plus facile de templater toute une structure en une seule expression Jinja. La forme mappage est plus facile à lire six mois plus tard.

boot a deux générations de syntaxe. Le module accepte les lettres héritées, où boot: "cdn" veut dire « essayer disque, puis CD-ROM, puis réseau ». Le PVE actuel veut une liste ordonnée explicite — boot: "order=scsi0;sata0;net0" — qui est sans ambiguïté sur quel disque. La forme héritée marche encore. La forme explicite est celle que vous voulez dans un nouveau travail. Un vrai piège enfoui dans la doc du module : le démarrage réseau exige de régler rng0 depuis PVE 8.3.5.

numa et numa_enabled sont des paramètres différents. numa_enabled est le booléen qui active NUMA. numa est un dict décrivant une topologie (cpus, hostnodes, memory, policy). Mettre numa: true est une erreur de type, et c’est une heure facile à perdre. Si vous voulez savoir pourquoi tout cela compte, l’alignement NUMA sur Proxmox couvre le problème sous-jacent.

machine: q35 et bios: ovmf sont les bons défauts, pas de la décoration — cet argument en entier.

Omettre vmid veut dire que le module demande à l’API le prochain ID libre. Pratique, et la cause directe de la chose suivante.

Pourquoi serial: 1

« Récupérer le prochain ID disponible, puis créer une VM avec » est deux appels d’API avec un intervalle au milieu. Deux workers faisant cela en même temps peuvent lire le même ID libre, et le perdant obtient une erreur ou, pire, une surprise.

serial: 1 fait la phase de création un hôte à la fois. Ce n’est pas rapide et ça n’a pas besoin de l’être. La partie coûteuse de la construction d’une VM se passe après que ce playbook a passé la main. Le play homologue qui démarre les VM finies utilise serial: 5, parce que le démarrage n’a pas de compteur partagé à disputer.

Si vous préférez le parallélisme, allouez le VMID vous-même depuis votre source de vérité et passez-le explicitement. Alors il n’y a pas de lire-modifier-écrire et pas de course.

L’idempotence n’est pas ce que vous attendez

C’est la section à lire deux fois, parce que proxmox_kvm ne se comporte pas comme ansible.builtin.package.

name n’est pas une identité. Les noms de VM ne sont pas uniques dans un cluster Proxmox, et le module le dit. Avec state: present et pas de vmid, si une VM de ce nom existe déjà, le module sort changed=false avec msg: "VM with name <x> already exists" et ne fait rien. Il ne compare pas vos paramètres à la réalité. Il ne converge pas. Il refuse.

update vaut false par défaut. Donc éditer memory: dans votre playbook et relancer est sans effet. La VM garde la mémoire avec laquelle elle a été construite, la tâche rapporte un succès, et rien nulle part ne vous dit que les deux ont divergé.

update: true refuse encore les paramètres intéressants. D’après la documentation du module :

Because of the operations of the API and security reasons, I have disabled the update of the following parameters net, virtio, ide, sata, scsi. Per example updating net update the MAC address and virtio create always new disk…

update_unsafe: true lève cette restriction, et l’avertissement n’est pas pour la forme :

Use this option with caution because an improper configuration might result in a permanent loss of data (for example disk recreated).

Donc le disque que vous pensiez redimensionner peut être remplacé par un nouveau vide. Ne recourez pas à ceci — les paramètres refusés ont leurs propres modules, et c’est la section suivante.

Et --check ne couvre rien de tout cela. La collection déclare la prise en charge du mode check par module, et elle est incohérente exactement dans le mauvais sens :

Modulecheck_modediff_mode
proxmox_kvmnonenone
proxmox_disknonenone
proxmox_templatenonenone
proxmox_snapfullnone
proxmox_nicfullnone
proxmox_poolfullnone

Le partage n’est pas « les modules en lecture seule le peuvent, les modules en écriture non » — proxmox_nic crée et supprime des interfaces et honore parfaitement bien le mode check. C’est que les trois modules traitant du stockage et du cycle de vie de la VM ne le font pas. Une passe --check d’un playbook de construction saute la création de la VM en silence puis rapporte sur un monde où la VM n’a jamais été faite, donc chaque tâche après elle raisonne sur le mauvais état. Sur un playbook de construction, --check n’est pas un filet de sécurité, et le traiter comme tel est pire que de ne pas le lancer.

Ce qu'une seconde passe de proxmox_kvm fait réellementseconde passe — state: present, et une VM de ce nom existele module ne compare jamais vos paramètres à la VM en marcheupdate: falsele défautchanged = false“VM with name <x>already exists”éditez memory dans le play,relancez, et rienne vous prévientupdate: trueconverge l'essentielcores, memory, tags,agent, onboot appliquésnet, virtio, ide, sata,scsi, efidisk0, tpmstate0refusés par conceptionutilisez proxmox_disk etproxmox_nic pour ceux-làupdate_unsafe: trueconverge toutles paramètres refuséssont appliqués aussiun paramètre de disque peutrecréer le disqueperte de données permanenteest le risque documenté--checkne vous dit riencheck_mode: nonela tâche est sautéechaque tâche suivanteraisonne sur un mondeoù la VM n'ajamais été crééeDeux des quatre convergent quoi que ce soit, et celle quicouvre les disques est celle qui peut les détruire.Verrouillez donc sur l'existence vous-même, et traitez la création enévénement unique.
Ce qu’une seconde passe fait réellement. Deux des quatre chemins convergent quoi que ce soit, et le seul qui couvre les disques est celui qui peut les détruire.

Alors faites de l’existence le verrou

Vu tout cela, le schéma qui marche est de cesser de demander au module d’être idempotent et de décider vous-même s’il faut construire. Le playbook de production le fait ainsi :

- name: Check if VM is present or manually built
  delegate_to: localhost
  community.proxmox.proxmox_vm_info:
    api_host: "{{ proxmox_api_ip }}"
    api_user: "{{ proxmox_user }}"
    api_password: "{{ proxmox_password }}"
    name: "{{ inventory_hostname }}"
    config: current
  register: existing_vm
  ignore_errors: true
  failed_when: (existing_vm.proxmox_vms | length) == 0

- name: Configure Proxmox VM
  when: existing_vm is failed
  block:
    - name: Create Proxmox VM
      ...

proxmox_vm_info avec config: current retourne la VM et sa configuration en vie, ou une liste vide. failed_when transforme « liste vide » en échec, ignore_errors: true empêche cet échec de terminer le play, et when: existing_vm is failed devient « la VM n’est pas là, construis-la ».

Se servir d’une tâche délibérément échouée comme d’un booléen se lit mal, et je ne vais pas prétendre le contraire. L’alternative est when: (existing_vm.proxmox_vms | default([]) | length) == 0, qui est honnête sur le fait d’être une vérification de longueur et n’a pas besoin d’ignore_errors. Les deux marchent. La version ci-dessus est ce qui est en production, et son seul vrai avantage est que le résultat enregistré porte la configuration existante pour que des tâches ultérieures la lisent.

La partie importante est la forme, pas l’orthographe : vérifier, puis bifurquer, et traiter la création comme un événement unique. La configuration continue d’une VM est un problème différent de l’existence d’une VM, et ce module n’est bon qu’au second.

Les disques et les NIC ont leurs propres modules

Voici la chose que j’ai passée sous silence plus haut, et elle change tout le tableau : les paramètres que proxmox_kvm refuse de mettre à jour ne sont pas un manque dans la collection. Ils sont délégués. community.proxmox.proxmox_disk et community.proxmox.proxmox_nic ajoutent, changent et retirent exactement les choses que le module de création ne touchera pas — clés sur les mêmes noms scsi0 et net0 que vous avez utilisés à la construction de la VM.

Les deux se comportent mieux que proxmox_kvm, et l’un d’eux est le seul module de ce flux qui peut être passé à blanc.

proxmox_nic — ajouter, réétiqueter ou retirer une interface

- name: Move the VM's primary NIC to a new bridge and VLAN
  delegate_to: localhost
  community.proxmox.proxmox_nic:
    api_host: "{{ proxmox_api_ip }}"
    api_user: ansible@pve
    api_token_id: automation
    api_token_secret: "{{ proxmox_token_secret }}"
    vmid: "{{ created_vm.vmid }}"
    interface: net0
    bridge: vmbr1
    tag: 120
    model: virtio
    mtu: 1
    queues: 4
    firewall: true
    state: present

interface est la seule option requise au-delà de l’authentification — net[n] où n va de 0 à 31 — et state: present ou absent vous donne l’ajout et le retrait. model vaut virtio par défaut, ce qui est la bonne réponse à moins qu’un invité ne s’en accommode pas.

La raison d’être de ce module est l’adresse MAC. Rappelez-vous pourquoi proxmox_kvm refuse de mettre à jour net : « updating net update the MAC address ». proxmox_nic corrige cela explicitement :

When not specified this module will keep the MAC address the same when changing an existing interface.

Vous pouvez donc réétiqueter un VLAN, déplacer un pont, changer le MTU ou activer le pare-feu sans que l’identité de la NIC de l’invité ne change en dessous. Cela compte plus qu’il n’y paraît : une nouvelle MAC invalide les réservations DHCP, casse tout ce qui est licencié à une NIC, et désynchronise l’enregistrement d’interface NetBox que le playbook de création a si soigneusement écrit. C’est le module qui rend banal un changement réseau en jour 2.

Quelques options à connaître avant d’en avoir besoin :

  • rate est en Mo/s — mégaoctets par seconde, pas en bits. La documentation est explicite et l’erreur de facteur huit est très facile à faire.
  • link_down: true déconnecte l’interface, décrite dans la doc comme « like pulling the plug » — comme débrancher la prise. Une façon propre d’isoler une VM suspecte sans l’arrêter ni toucher l’invité.
  • trunks prend une liste d’ID de VLAN à laisser passer, pour un invité qui fait son propre étiquetage.
  • mtu: 1 n’est pas une faute de frappe et pas un MTU de 1 octet — cela veut dire « hériter du MTU du pont », et ne s’applique qu’à virtio.
  • queues règle le multiqueue, 0 à 16. Vaut d’être assorti au nombre de vCPU sur tout ce qui pousse du vrai trafic.

Et il prend le mode check pleinement en charge. --check sur une tâche proxmox_nic vous dit la vérité, ce qui en fait la seule partie de ce flux que vous pouvez répéter en sûreté. Ses messages sont proprement idempotents aussi. Une interface inchangée rapporte Nic net0 unchanged on VM with vmid 103 plutôt que de revendiquer un changement.

proxmox_disk — tout le cycle de vie du disque

proxmox_disk est le plus grand module des trois, et son state fait cinq travaux différents :

stateCe qui se passeRéversible ?
presentcrée le disque, ou met à jour les options d’un existants.o.
resizedl’agrandit — PVE ne peut pas rétrécir, et la doc dit de le faire à la mainnon
detacheddevient unused[n] ; le volume et ses données restentoui
movedchange le stockage d’appui, ou remet le disque à une autre VMl’original gardé sauf delete_moved
absentretiré du stockage d’appuinon

L’écart entre detached et absent est le filet de sécurité que proxmox_kvm ne vous donne jamais. Détacher est un changement de configuration ; supprimer détruit des données. Deux mots différents, deux conséquences différentes.

Ajouter un second disque à une VM qui existe déjà :

- name: Add a data disk
  delegate_to: localhost
  community.proxmox.proxmox_disk:
    api_host: "{{ proxmox_api_ip }}"
    api_user: ansible@pve
    api_token_id: automation
    api_token_secret: "{{ proxmox_token_secret }}"
    vmid: "{{ created_vm.vmid }}"
    disk: scsi1
    storage: vmdata
    size: 200
    format: qcow2
    iothread: true
    aio: io_uring
    discard: "on"
    ssd: true
    backup: true
    state: present

create est le bouton que proxmox_kvm aurait dû avoir. Il contrôle ce que state: present a le droit de faire :

  • regular (le défaut) — crée le disque s’il manque, sinon met à jour ses options.
  • disabled — met à jour les options seulement, et ne crée jamais. C’est celui à choisir quand vous changez cache ou iothread sur un disque qui doit déjà exister. Il ne peut pas vous surprendre en invoquant un nouveau volume parce qu’une clé était mal orthographiée.
  • forced — crée toujours. Un disque existant est détaché et laissé inutilisé, pas supprimé.

Ce dernier comportement est le détail important. create: forced est l’option à l’air destructeur, et elle ne détruit quand même rien : l’ancien volume survit en unusedN et vous pouvez le rattacher. Comparez cela avec proxmox_kvm plus update_unsafe, dont le mode d’échec documenté est un disque recréé. Même opération grossière, bien meilleur rayon d’explosion.

Agrandir un disque :

- name: Grow the data disk by 100 GiB
  delegate_to: localhost
  community.proxmox.proxmox_disk:
    api_host: "{{ proxmox_api_ip }}"
    api_user: ansible@pve
    api_token_id: automation
    api_token_secret: "{{ proxmox_token_secret }}"
    vmid: "{{ created_vm.vmid }}"
    disk: scsi1
    size: "+100G"
    state: resized

Attention aux unités, parce que size change de sens avec state. Avec state: present c’est des Gio en nombre nu (size: 200). Avec state: resized il prend un suffixe — +100G pour ajouter à la taille actuelle, ou 500G comme cible absolue. Un paramètre, deux conventions, et l’échec est silencieux si vous devinez mal.

Déplacer un disque vers un stockage différent, ce qui est le cas de la migration-à-chaud-d’un-seul-volume :

- name: Move the disk to NVMe storage
  delegate_to: localhost
  community.proxmox.proxmox_disk:
    api_host: "{{ proxmox_api_ip }}"
    api_user: ansible@pve
    api_token_id: automation
    api_token_secret: "{{ proxmox_token_secret }}"
    vmid: "{{ created_vm.vmid }}"
    disk: scsi1
    target_storage: nvme-pool
    bwlimit: 200000
    delete_moved: true
    timeout: 3600
    state: moved

target_storage déplace au sein d’une VM ; target_vmid remet le disque à une VM différente et exige le même stockage des deux côtés. Ils sont mutuellement exclusifs. delete_moved vaut false par défaut, donc par défaut vous finissez avec deux copies et l’original qui siège là comme inutilisé — sûr, et une bonne façon de remplir un pool de stockage si vous n’y revenez jamais.

timeout vaut 600 ici par défaut, contre 30 dans proxmox_kvm. Paramètre d’apparence identique, différence d’un facteur vingt, parce que ces opérations copient des données. Relevez-le pour de grandes images ou du stockage lent — la doc le dit pour moved comme pour import_from.

Ce qui amène l’option qui fait de ce module le chemin V2V et image cloud :

    import_from: "vmdata:9000/base-debian13.qcow2"

import_from bâtit le disque depuis un volume existant plutôt que d’en allouer un vide — <STORAGE>:<VMID>/<NAME>, ou <STORAGE>:import/<NAME> en utilisant le répertoire d’import de stockage sur PVE 9.x et plus tard. Il est mutuellement exclusif avec size, et seul root peut utiliser des chemins de système de fichiers absolus.

Le reste de la liste de paramètres est la raison d’attacher les disques avec ce module plutôt qu’en ligne dans l’appel de création : cache, aio, iothread, discard, ssd, backup, detect_zeroes, et toute la famille de limitation — iops, iops_rd, iops_wr, leurs variantes _max et _max_length, et les contrôles de rafale bps_*_max_length. Rien de tout cela n’est atteignable par proxmox_kvm après la création.

Deux réserves, toutes deux de la propre documentation du module :

  • Certains changements d’options ont besoin d’un redémarrage. « Some updates on options (like cache) are not being applied instantly and require VM restart. » Une tâche verte veut dire que la configuration a été écrite, pas que la VM en marche se comporte différemment.
  • Il ne prend pas le mode check en charge. check_mode: none, comme proxmox_kvm. La collection se coupe donc en deux : les changements de NIC peuvent être répétés avec --check, les changements de disque non.

La division du travail

Pour faire ceciUtilisez
Créer la VMproxmox_kvm, une fois, verrouillé sur l’existence
Changer cœurs, mémoire, tags, agent, onbootproxmox_kvm avec update: true
Ajouter, réétiqueter, déconnecter ou retirer une NICproxmox_nic
Ajouter, agrandir, déplacer, détacher ou retirer un disqueproxmox_disk
Instantanéproxmox_snap (aussi le mode check complet)
Tout ce qu’aucun d’eux n’exposeqm set par SSH
Changer un disque ou une NIC par proxmox_kvmrien — c’est à cela que sert update_unsafe, et c’est pourquoi vous ne devriez pas l’utiliser

Construisez la VM avec un appel proxmox_kvm minimal, puis attachez les disques et les interfaces avec leurs propres modules. C’est plus de tâches, et c’est la version où les changements en jour 2 ont une route qui n’implique pas une option dont le risque documenté est de perdre un disque.

Là où le module s’arrête

proxmox_kvm a une énorme liste de paramètres et ne couvre quand même pas tout ce que qm peut faire. Plutôt que d’attendre, le playbook de production descend à la CLI sur le nœud :

- name: Set RNG source and better SPICE quality
  delegate_to: "{{ proxmox_api_host }}"
  become: true
  ansible.builtin.command:
    cmd: >-
      /usr/sbin/qm set {{ created_vm.vmid }}
      --rng0 source=/dev/urandom
      --spice_enhancements videostreaming=all

Il n’y a rien de mal à cela. Ce n’est idempotent en aucun sens significatif — qm set est une écriture, et il rapportera changed à chaque passe — mais c’est explicite, c’est lisible, et ça ne fait pas semblant. Si un module gagne le paramètre plus tard, vous supprimez la tâche.

D’autres choses ont besoin d’une seconde passe par le module avec update: true, parce qu’elles ne peuvent pas être réglées dans le même appel qui crée la VM :

- name: Add SPICE-compatible USB device
  delegate_to: localhost
  community.proxmox.proxmox_kvm:
    api_host: "{{ proxmox_api_ip }}"
    api_user: "{{ proxmox_user }}"
    api_password: "{{ proxmox_password }}"
    node: "{{ proxmox_api_host }}"
    vmid: "{{ created_vm.vmid }}"
    usb:
      usb0: "spice,usb3=1"
    update: true
  when: spice_usb | default(false)

Notez qu’il passe vmid, pas name. Une fois que vous avez l’ID, utilisez-le. C’est le seul identifiant que l’API traite comme unique.

Et pour les choses que QEMU peut faire et que PVE n’a pas d’option pour, il y a args, qui est passé à la ligne de commande QEMU verbatim :

    args: >-
      -global scsi-hd.physical_block_size=4k
      -global scsi-hd.logical_block_size=4096

Celui-là présente le disque virtuel en 4Kn plutôt qu’en 512e, ce qui compte plus qu’il n’en a l’air — tailles de bloc, 4Kn et 512e. Le module étiquette args « for experts only » — pour experts seulement — et la raison est que PVE ne le valide pas et qu’un mauvais drapeau empêche la VM de démarrer avec une erreur qui vient de QEMU plutôt que de Proxmox.

Le cluster n’est pas instantanément cohérent

- name: Let registration complete on cluster
  ansible.builtin.pause:
    seconds: 5
  when: created_vm.changed

Un pause dans un playbook est d’ordinaire une mauvaise odeur, et celui-ci est porteur. L’appel de création revient quand l’API a accepté la définition, ce qui n’est pas la même chose que chaque nœud s’accordant sur l’existence de la VM — et la toute prochaine tâche veut poser une ACL sur /vms/<vmid>. Cinq secondes de patience sont moins chères qu’une boucle de réessai autour d’une erreur qui n’apparaît que sous charge.

Le playbook de démantèlement a la même forme pour la même raison : arrêter, attendre, puis supprimer.

Relire ce que vous avez construit

proxmox_kvm documente trois valeurs de retour : vmid, status et msg. En pratique vous en voudrez une quatrième, et elle n’est pas dans la documentation.

- name: Get MAC address of VM
  ansible.builtin.set_fact:
    primary_mac_addr: "{{ created_vm.mac.net0 }}"

created_vm.mac est réel — le module le bâtit dans get_vminfo() et le colle dans le résultat — mais il est absent du bloc RETURN documenté, ce qui veut dire que rien ne promet qu’il continuera de marcher. Il vaut de savoir exactement comment il se comporte, parce qu’il y a deux pièges dedans :

  1. Il n’apparaît que quand le module a réellement créé la VM. mac n’est assemblé que sur le chemin création-et-déploiement. Prenez la branche « existe déjà » et le résultat a vmid et msg et rien d’autre.
  2. Il ne contient que les interfaces que vous avez passées. Le code parcourt les paramètres que vous avez fournis et en extrait ceux correspondant à net[0-9], puis relit la configuration stockée de chacun depuis l’API. Pas de paramètre net, pas de clé mac.

C’est pourquoi le playbook de production a besoin des deux moitiés, et la seconde est laide :

- name: Get MAC address of VM
  ansible.builtin.set_fact:
    primary_mac_addr: >-
      {{ created_vm.mac.net0 if created_vm is defined and created_vm.changed
         else ((existing_vm.proxmox_vms[0].config.net0 | split(','))[0] | split('='))[1] }}

Quand la VM existait déjà, il n’y a pas de mac, donc la MAC doit être extraite de la chaîne de configuration brute. net0 revient de l’API comme virtio=AE:AE:5C:A8:89:85,bridge=vmbr0, donc : découper sur les virgules, prendre le premier champ, découper sur =, prendre la seconde moitié. C’est de la chirurgie de chaîne sur une réponse d’API, et c’est le coût honnête d’un module dont la forme de retour dépend de la branche prise.

Si vous avez besoin de la MAC de façon fiable dans les deux cas, prenez-la de proxmox_vm_info inconditionnellement et analysez une seule forme plutôt que deux.

Le piloter depuis une source de vérité

Regardez encore la ligne qui ouvre le play :

hosts: "{{ target_hosts | default('cluster_pve:&status_planned') }}"

C’est l’architecture réelle, et il vaut de le dire clairement : les VM à construire ne sont pas une liste dans un fichier de vars. Ce sont les hôtes de votre inventaire dont le statut enregistré dit qu’ils devraient exister et ne le font pas encore.

L’inventaire ici est NetBox. Une VM est demandée en créant un enregistrement NetBox de statut planned, portant son CPU, sa mémoire, son disque, son VLAN, son propriétaire et sa plateforme. Le playbook sélectionne les machines planned, les construit, alloue une IP, écrit le DNS, puis passe l’enregistrement à staged — moment où cet hôte ne correspond plus au motif d’hôtes du play, et un handler rafraîchit l’inventaire pour que le prochain play voie le nouvel état :

handlers:
  - name: Refresh inventory
    ansible.builtin.meta: refresh_inventory

Le champ de statut est une machine à états, le playbook est une transition dedans, et le tout est relançable parce qu’un hôte qui a déjà avancé n’est plus sélectionné. C’est une bien meilleure propriété que n’importe quelle idempotence au niveau du module, et c’est la raison pour laquelle la tâche de création peut se permettre d’être à coup unique.

community.proxmox livre aussi son propre plugin d’inventaire, qui construit un inventaire depuis le cluster — le bon choix quand Proxmox est la source de vérité. Ici c’est l’inverse : NetBox fait autorité et Proxmox est là où son intention se réalise. C’est tout un billet à part et je l’écrirai séparément.

Le reprendre

La création sans démantèlement est un demi-cycle de vie, et le chemin de retrait a son propre piège — vous ne pouvez pas supprimer une VM en marche :

- name: Force stop the VM if it is running
  delegate_to: localhost
  community.proxmox.proxmox_kvm:
    api_host: "{{ proxmox_api_ip }}"
    api_user: "{{ proxmox_user }}"
    api_password: "{{ proxmox_password }}"
    name: "{{ inventory_hostname }}"
    state: stopped
    force: true
    timeout: 10

- name: Allow the cluster to stop the VM before removing it
  ansible.builtin.pause:
    seconds: 10

- name: Remove the VM from the cluster
  delegate_to: localhost
  community.proxmox.proxmox_kvm:
    api_host: "{{ proxmox_api_ip }}"
    api_user: "{{ proxmox_user }}"
    api_password: "{{ proxmox_password }}"
    name: "{{ inventory_hostname }}"
    state: absent
    force: true
    timeout: 10

state: stopped est un arrêt gracieux, et l’interaction avec timeout est documentée et vaut d’être mémorisée : si le délai est atteint avec force: true, la VM est éteinte durement ; avec force: false, la tâche échoue à la place. Une fenêtre gracieuse de dix secondes suivie d’un débranchage est une politique raisonnable pour une machine qu’on démantèle, et une terrible pour tout le reste.

Le démantèlement complet (proxmox-remove-vms.yml) défait ensuite le reste de l’enregistrement : le DNS, l’IP allouée, les interfaces NetBox, la VM NetBox, et les entrées périmées dans known_hosts. Il enveloppe le bloc dans ignore_errors: true, ce qui est défendable dans un démantèlement. Vous retirez des choses qui peuvent déjà être parties, et une machine à moitié supprimée est pire qu’un journal bruyant.

Ce que je changerais dans un montage neuf

Ayant lu la source du module plutôt que sa seule documentation, quatre choses :

  • Utilisez un jeton d’API, pas api_user plus api_password. Cadré, révocable, et il n’appartient jamais à une personne.
  • Réglez validate_certs explicitement, avant que 2.0.0 ne le change sous vous.
  • Allouez le VMID vous-même depuis la source de vérité. Cela retire la course lire-modifier-écrire, vous laisse abandonner serial: 1, et donne à chaque tâche ultérieure un identifiant stable au lieu d’un nom qui n’est pas unique.
  • Créez la VM nue, puis attachez ses disques et NIC avec proxmox_disk et proxmox_nic. Plus de tâches, mais chaque disque et interface a alors un module qui peut le changer plus tard — dont create: disabled pour des éditions d’options seulement et state: detached au lieu de la suppression — plutôt qu’une configuration qui ne peut être changée que par update_unsafe.
  • Prenez la MAC de proxmox_vm_info à un seul endroit, pour qu’il y ait une seule forme à analyser au lieu d’une conditionnelle entre une valeur de retour documentée et une non documentée.

Et ne recourez pas à --check sur un playbook de construction. Le module qui compte ne peut pas l’honorer.

Une passe à blanc qui dit toujours oui est pire que pas de passe à blanc du tout, parce que vous la croirez.

Références