Die Annahme, die Ansible sonst machen darf
Fast jedes Ansible-Modul, das du benutzt hast, arbeitet so: Ansible verbindet sich zu dem Host, der im Inventar steht, kopiert ein kleines Python-Programm dorthin, führt es aus und liest das Ergebnis zurück. Der Host ist das Ding, das geändert wird, und das Ding, das die Arbeit macht.
Eine virtuelle Maschine anzulegen bricht das auf die grundlegendste Weise. Den Host, den du baust, gibt es nicht. Er hat keine IP, keinen SSH-Dienst, kein Python und kein Betriebssystem. Es ist nichts da, zu dem man verbinden könnte.
community.proxmox ist also kein Konfigurationsagent. Es ist ein API-Client, der zufällig als Ansible-Collection ausgeliefert wird. Alles in diesem Beitrag folgt aus dieser einen Tatsache — wo die Tasks laufen, wie du Zugangsdaten hineinbekommst, warum ein zweiter Lauf nicht tut, was du erwartest, und warum --check dir nicht die Wahrheit sagt.
Die Beispiele hier sind eingekürzt aus einem Playbook, das Windows- und Linux-VMs aus NetBox-Datensätzen baut: proxmox-create-vms.yml. Ich habe die Namen von Knoten, Speicher und Bridge der Lesbarkeit zuliebe verallgemeinert — das Echte liegt in diesem Repository.
Alles, was ich unten über das Verhalten der Module behaupte, wurde gegen community.proxmox 1.6.0 geprüft, die Fassung, die ich installiert habe:
$ ansible-galaxy collection list community.proxmox
# /home/damien/.ansible/collections/ansible_collections
Collection Version
----------------- -------
community.proxmox 1.6.0
Zuerst: die Collection ist umgezogen
Liest du ein älteres Playbook oder eine ältere Antwort, hießen die Module community.general.proxmox_kvm. Sie wohnen jetzt in einer eigenen Collection, und dort findet die Entwicklung statt. Fassung 1.6.0 liefert 47 Module, die Ceph, SDN, Firewall, HA-Regeln und den Cluster-Beitritt abdecken, und nichts davon gab es in der Zeit von community.general.
ansible-galaxy collection install community.proxmox
pip install 'proxmoxer>=2.0' requests
Die Python-Abhängigkeit ist nicht wahlfrei und nicht mitgeliefert: die Collection erklärt requirements: ["proxmoxer >= 2.0", "requests"], und die müssen dort installiert sein, wo das Modul tatsächlich ausgeführt wird — und das ist, wie der nächste Abschnitt erklärt, nicht der Proxmox-Knoten.
requirements.yml, wenn du es lieber festnagelst:
---
collections:
- name: community.proxmox
version: ">=1.6.0"
Die Collection wird gegen ansible-core 2.17 bis 2.20 getestet. Deine community.general.proxmox_*-Tasks in community.proxmox.proxmox_* umzubenennen ist der größte Teil der Umstellung.
Jeder Proxmox-Task läuft auf localhost
Zwei Zeilen am Kopf des Plays tragen die Last, und beide sehen aus, als würden sie etwas Nützliches abschalten:
- name: Create Proxmox virtual machines
hosts: "{{ target_hosts | default('cluster_pve:&status_planned') }}"
gather_facts: false
serial: 1
gather_facts: false ist keine Optimierung. Das Sammeln von Fakten verbindet sich zum Inventar-Host, und der Inventar-Host ist eine VM, die noch nicht gebaut ist. Lass es an, und das Play scheitert vor dem ersten Task.
Dann trägt jeder Proxmox-Task ein 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 }}"
...
Der Inventar-Host ist jetzt bloß ein Name und ein Sack Variablen. inventory_hostname wird der Name der VM; seine Variablen beschreiben die Maschine, die du willst. Nichts verbindet sich dorthin. Der Task läuft auf dem Steuerknoten, der eine HTTPS-Sitzung zu api_host öffnet und eine VM-Definition abschickt.
Du wirst das auch als local_action: geschrieben sehen, die ältere Schreibweise für dasselbe. Das Abbau-Playbook in jenem Repository nutzt sie durchgehend. Sie sind gleichwertig. delegate_to ist die heutige Schreibweise.
Die Notluke zeigt auf den Knoten
Manches muss wirklich auf einem Proxmox-Host passieren, und diese Tasks delegieren woandershin:
- 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
Das ist eine echte SSH-Verbindung zu einem echten Knoten, die einen echten Pfad auf gemeinsamem Speicher prüft, denn die API nimmt bereitwillig eine ISO-Angabe an, die auf keine Datei zeigt, und das willst du lieber jetzt erfahren als beim Start. Beachte die Zeichenketten-Operation, die eine PVE-Speicherangabe (isos:iso/debian.iso) in einen Dateisystempfad überführt. Die Speicherabstraktion steht stat nicht zur Verfügung.
Ein einzelnes Play hat also Tasks, die an drei verschiedenen Stellen ausgeführt werden, und sie zu verwechseln ist die häufigste Weise, wie diese Playbooks scheitern:
qm-Notluke ist der einzige Teil, der SSH zu einem Hypervisor braucht.Zugangsdaten, und eine Voreinstellung, die sich bald ändert
Die Auth-Optionen teilen alle Module der Collection über ein Dokumentationsfragment, sie sind also überall dieselben: api_host, api_user, und dann entweder api_password oder das Paar api_token_id / api_token_secret. Alle greifen auf Umgebungsvariablen zurück — PROXMOX_HOST, PROXMOX_USER, PROXMOX_PASSWORD, PROXMOX_TOKEN_ID, PROXMOX_TOKEN_SECRET, PROXMOX_VALIDATE_CERTS —, und das ist der saubere Weg, Geheimnisse ganz aus dem Play zu halten.
Ein API-Token ist die bessere Voreinstellung. Es ist eingegrenzt, es ist widerrufbar, ohne das Passwort eines Menschen zu ändern, und es kann genau die Rechte bekommen, die das Playbook braucht, statt der Rechte, die eine Person zufällig hat:
- 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
Setz validate_certs ausdrücklich, und zwar heute. Die eigene Dokumentation der Collection sagt es klar:
Currently defaults to
falseand changes default totruewith community.proxmox 2.0.0.
Das heißt, ein Playbook, das es nie erwähnt, prüft TLS gerade nicht und wird anfangen zu prüfen — und damit anfangen, gegen das selbst ausgestellte Zertifikat zu scheitern, das jede frische Proxmox-Installation mitbringt —, sobald jemand --upgrade laufen lässt. Diese Entscheidung trifft man besser mit Absicht, als sie mitten in einem Aufbau zu erleben. Behältst du das selbst ausgestellte Zertifikat, schreib validate_certs: false und nimm den Befund hin; hast du eine richtige Kette, zeig mit ca_path darauf. So oder so steht es geschrieben.
Das kleinste taugliche Anlegen
Kürz den produktiven Task auf das, was eine Maschine tatsächlich beschreibt, und es ist lesbar:
- 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
Ein paar Dinge an dieser Form sind es wert, sie zu kennen, bevor du deine eigene schreibst.
Die Geräteoptionen sind PVE-Syntax innerhalb von YAML. scsi, sata, net, virtio, ide sind alle als dict typisiert, mit Schlüsseln scsi0, net0 und so weiter, und die Werte sind die kommagetrennten Optionszeichenketten direkt aus man qm — <storage>:<size>,option=value für eine Platte, [model=]<enum>,option=value für eine NIC. Das Modul modelliert sie nicht; es leitet sie weiter. Wird etwas abgelehnt, steht die Antwort in der PVE-Optionsreferenz, nicht in der Ansible-Dokumentation.
Du wirst diese auch als JSON-Zeichenkette statt als YAML-Abbildung sehen:
net: '{"net0":"virtio,bridge={{ vlan_bridge }}"}'
Beides funktioniert. Ansible wandelt die Zeichenkette für einen als dict typisierten Parameter um. Die JSON-Form existiert, weil es leichter ist, eine ganze Struktur in einem Jinja-Ausdruck zu erzeugen. Die Abbildungsform ist in sechs Monaten leichter zu lesen.
boot hat zwei Generationen Syntax. Das Modul nimmt die alten Buchstaben, wo boot: "cdn" heißt „versuch Platte, dann CD-ROM, dann Netz“. Das heutige PVE will eine ausdrücklich geordnete Liste — boot: "order=scsi0;sata0;net0" —, die eindeutig sagt, welche Platte. Die alte Form läuft weiter. Die ausdrückliche ist die, die du in neuer Arbeit willst. Ein echter Fallstrick, tief in der Moduldokumentation begraben: Netzstart braucht seit PVE 8.3.5, dass rng0 gesetzt ist.
numa und numa_enabled sind verschiedene Parameter. numa_enabled ist der Schalter, der NUMA einschaltet. numa ist ein Dict, das eine Topologie beschreibt (cpus, hostnodes, memory, policy). numa: true zu setzen ist ein Typfehler, und es ist eine leicht verlorene Stunde. Interessiert dich, warum das alles zählt, behandelt NUMA-Ausrichtung auf Proxmox das Problem darunter.
machine: q35 und bios: ovmf sind die richtigen Voreinstellungen, keine Zierde — das Argument in voller Länge.
vmid weglassen heißt, das Modul fragt die API nach der nächsten freien ID. Bequem, und der unmittelbare Grund für das Nächste.
Warum serial: 1
„Hol die nächste verfügbare ID, dann leg eine VM damit an“ sind zwei API-Aufrufe mit einer Lücke in der Mitte. Zwei Arbeiter, die das gleichzeitig tun, können dieselbe freie ID lesen, und der Verlierer bekommt einen Fehler oder, schlimmer, eine Überraschung.
serial: 1 macht die Anlege-Phase zu einem Host nach dem anderen. Es ist nicht schnell und muss es nicht sein. Der teure Teil des VM-Baus passiert, nachdem dieses Playbook übergeben hat. Das Gegenstück-Play, das die fertigen VMs startet, nutzt serial: 5, denn beim Starten gibt es keinen gemeinsamen Zähler, um den man sich streiten könnte.
Willst du lieber die Parallelität, teile die VMID selbst aus deiner verlässlichen Quelle zu und übergib sie ausdrücklich. Dann gibt es kein Lesen-Ändern-Schreiben und kein Rennen.
Idempotenz ist nicht, was du erwartest
Das ist der Abschnitt, den man zweimal liest, denn proxmox_kvm verhält sich nicht wie ansible.builtin.package.
name ist keine Identität. VM-Namen sind über einen Proxmox-Cluster hinweg nicht eindeutig, und das Modul sagt es. Mit state: present und ohne vmid steigt das Modul, wenn eine VM mit diesem Namen schon existiert, mit changed=false und msg: "VM with name <x> already exists" aus und tut nichts. Es vergleicht deine Parameter nicht mit der Wirklichkeit. Es konvergiert nicht. Es verweigert sich.
update steht voreingestellt auf false. memory: in deinem Playbook zu ändern und neu zu laufen ist also wirkungslos. Die VM behält den Speicher, mit dem sie gebaut wurde, der Task meldet Erfolg, und nirgends sagt dir etwas, dass die zwei auseinandergelaufen sind.
update: true verweigert die interessanten Parameter weiterhin. Aus der Moduldokumentation:
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 updatingnetupdate the MAC address andvirtiocreate always new disk…
update_unsafe: true hebt diese Einschränkung auf, und die Warnung ist keine Show:
Use this option with caution because an improper configuration might result in a permanent loss of data (for example disk recreated).
Die Platte, von der du dachtest, du würdest sie vergrößern, kann also durch eine neue leere ersetzt werden. Greif nicht danach — die verweigerten Parameter haben ihre eigenen Module, und das ist der nächste Abschnitt.
Und --check deckt nichts davon ab. Die Collection erklärt Check-Mode-Unterstützung je Modul, und sie ist genau in die falsche Richtung ungleichmäßig:
| Modul | check_mode | diff_mode |
|---|---|---|
proxmox_kvm | keiner | keiner |
proxmox_disk | keiner | keiner |
proxmox_template | keiner | keiner |
proxmox_snap | voll | keiner |
proxmox_nic | voll | keiner |
proxmox_pool | voll | keiner |
Die Trennung ist nicht „lesende Module können es, schreibende nicht“ — proxmox_nic legt Schnittstellen an und löscht sie und achtet den Check-Mode einwandfrei. Sie ist, dass die drei Module, die mit Speicher und VM-Lebenszyklus zu tun haben, es nicht tun. Ein --check-Lauf eines Bau-Playbooks überspringt das Anlegen der VM still und berichtet dann über eine Welt, in der die VM nie gemacht wurde, sodass jeder Task danach über den falschen Zustand nachdenkt. Auf einem Bau-Playbook ist --check kein Auffangnetz, und es dafür zu nehmen ist schlimmer, als es nicht laufen zu lassen.
Also mach die Existenz zum Tor
Angesichts all dessen ist das gangbare Muster, das Modul nicht länger um Idempotenz zu bitten und selbst zu entscheiden, ob gebaut wird. Das produktive Playbook macht es so:
- 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 mit config: current gibt die VM und ihre laufende Konfiguration zurück, oder eine leere Liste. failed_when macht aus „leere Liste“ einen Fehlschlag, ignore_errors: true hindert diesen Fehlschlag daran, das Play zu beenden, und when: existing_vm is failed wird zu „die VM ist nicht da, bau sie“.
Einen absichtlich fehlgeschlagenen Task als Wahrheitswert zu nutzen liest sich schlecht, und ich will das nicht anders darstellen. Die Alternative ist when: (existing_vm.proxmox_vms | default([]) | length) == 0, was ehrlich dazu steht, eine Längenprüfung zu sein, und kein ignore_errors braucht. Beides läuft. Die Fassung oben ist die, die produktiv ist, und ihr einziger echter Vorteil ist, dass das registrierte Ergebnis die vorhandene Konfiguration für spätere Tasks mitbringt.
Der wichtige Teil ist die Form, nicht die Schreibweise: prüfen, dann verzweigen, und das Anlegen als einmaliges Ereignis behandeln. Die laufende Konfiguration einer VM ist ein anderes Problem als die Existenz einer VM, und dieses Modul ist nur im Zweiten gut.
Platten und NICs haben ihre eigenen Module
Hier ist das, was ich oben übergangen habe, und es verändert das ganze Bild: die Parameter, die proxmox_kvm nicht aktualisieren will, sind keine Lücke in der Collection. Sie sind abgegeben. community.proxmox.proxmox_disk und community.proxmox.proxmox_nic legen genau die Dinge an, ändern sie und entfernen sie, die das Anlege-Modul nicht anfassen will — anhand derselben Namen scsi0 und net0, die du beim Bauen der VM benutzt hast.
Beide sind besser erzogen als proxmox_kvm, und eines von ihnen ist das einzige Modul in diesem Ablauf, das man trocken laufen lassen kann.
proxmox_nic — eine Schnittstelle anlegen, umtaggen oder entfernen
- 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 ist die einzige nötige Option über die Auth hinaus — net[n], wobei n von 0 bis 31 geht —, und state: present oder absent gibt dir Anlegen und Entfernen. model steht voreingestellt auf virtio, was die richtige Antwort ist, solange ein Gast nicht damit überfordert ist.
Der Grund, warum dieses Modul existiert, ist die MAC-Adresse. Erinnere dich, warum proxmox_kvm sich weigert, net zu aktualisieren: „updating net update the MAC address“. proxmox_nic behebt das ausdrücklich:
When not specified this module will keep the MAC address the same when changing an existing interface.
Du kannst also ein VLAN umtaggen, eine Bridge wechseln, die MTU ändern oder die Firewall einschalten, ohne dass sich die NIC-Identität des Gastes darunter ändert. Das zählt mehr, als es klingt: eine neue MAC macht DHCP-Reservierungen ungültig, bricht alles, was auf eine NIC lizenziert ist, und bringt den NetBox-Schnittstellendatensatz aus dem Tritt, den das Anlege-Playbook so sorgfältig geschrieben hat. Das ist das Modul, das eine Netzänderung am zweiten Tag langweilig macht.
Ein paar Optionen, die es wert sind, bevor du sie brauchst:
rateist in MB/s — Megabyte pro Sekunde, nicht Bit. Die Dokumentation ist ausdrücklich, und der Faktor-Acht-Fehler ist sehr leicht gemacht.link_down: truetrennt die Schnittstelle, in der Dokumentation beschrieben als „like pulling the plug“. Ein sauberer Weg, eine verdächtige VM zu isolieren, ohne sie zu stoppen oder den Gast anzufassen.trunksnimmt eine Liste von VLAN-IDs zum Durchlassen, für einen Gast, der selbst taggt.mtu: 1ist kein Tippfehler und keine MTU von 1 Byte — es heißt „erbe die MTU der Bridge“, und es gilt nur fürvirtio.queuessetzt Multi-Queue, 0 bis 16. Bei allem, was echten Verkehr schiebt, lohnt es sich, das auf die vCPU-Zahl abzustimmen.
Und es unterstützt den Check-Mode voll. --check auf einem proxmox_nic-Task sagt dir die Wahrheit, und das macht ihn zum einzigen Teil dieses Ablaufs, den du sicher üben kannst. Seine Meldungen sind auch richtig idempotent. Eine unveränderte Schnittstelle meldet Nic net0 unchanged on VM with vmid 103, statt eine Änderung zu behaupten.
proxmox_disk — der ganze Lebenszyklus einer Platte
proxmox_disk ist das größte der drei Module, und sein state macht fünf verschiedene Aufgaben:
state | Was passiert | Umkehrbar? |
|---|---|---|
present | die Platte anlegen, oder Optionen an einer vorhandenen ändern | entfällt |
resized | vergrößern — PVE kann nicht schrumpfen, und die Dokumentation sagt, das von Hand zu machen | nein |
detached | wird unused[n]; das Volume und seine Daten bleiben | ja |
moved | den Speicher darunter wechseln, oder die Platte einer anderen VM geben | Original bleibt, außer bei delete_moved |
absent | aus dem Speicher darunter entfernt | nein |
Der Abstand zwischen detached und absent ist das Auffangnetz, das proxmox_kvm dir nie gibt. Abtrennen ist eine Konfigurationsänderung; Löschen zerstört Daten. Zwei verschiedene Wörter, zwei verschiedene Folgen.
Eine zweite Platte an eine schon vorhandene VM hängen:
- 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 ist das Stellrad, das proxmox_kvm hätte haben sollen. Es steuert, was state: present tun darf:
regular(die Voreinstellung) — die Platte anlegen, wenn sie fehlt, sonst ihre Optionen ändern.disabled— nur Optionen ändern und nie anlegen. Das ist das, wonach man greift, wenn mancacheoderiothreadan einer Platte ändert, die schon existieren muss. Es kann dich nicht überraschen, indem es wegen eines vertippten Schlüssels ein neues Volume herbeizaubert.forced— immer anlegen. Eine vorhandene Platte wird abgetrennt und unbenutzt liegen gelassen, nicht gelöscht.
Das letzte Verhalten ist die wichtige Einzelheit. create: forced ist die zerstörerisch aussehende Möglichkeit, und sie zerstört dennoch nichts: das alte Volume überlebt als unusedN, und du kannst es wieder anhängen. Vergleich das mit proxmox_kvm plus update_unsafe, dessen dokumentierter Fehlerfall eine neu angelegte Platte ist. Ungefähr derselbe Vorgang, viel bessere Wirkungsweite.
Eine Platte vergrößern:
- 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
Achte auf die Einheiten, denn size ändert mit state seine Bedeutung. Mit state: present ist es GiB als nackte Zahl (size: 200). Mit state: resized nimmt es ein Suffix — +100G, um zur aktuellen Größe zu addieren, oder 500G als absolutes Ziel. Ein Parameter, zwei Konventionen, und der Fehlschlag ist still, wenn du falsch rätst.
Eine Platte auf anderen Speicher verschieben, der Fall der Live-Migration eines einzelnen Volumes:
- 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 verschiebt innerhalb einer VM; target_vmid gibt die Platte einer anderen VM und verlangt denselben Speicher auf beiden. Sie schließen sich gegenseitig aus. delete_moved steht voreingestellt auf false, du endest also standardmäßig mit zwei Kopien und dem Original, das unbenutzt herumliegt — sicher, und ein guter Weg, einen Speicherpool zu füllen, wenn du nie zurückkommst.
timeout steht hier voreingestellt auf 600, gegen 30 in proxmox_kvm. Gleich aussehender Parameter, zwanzigfacher Unterschied, denn diese Vorgänge kopieren Daten. Setz ihn für große Abbilder oder langsamen Speicher höher — die Dokumentation sagt das für moved und für import_from.
Was zu der Option führt, die dieses Modul zum Weg für V2V und Cloud-Abbilder macht:
import_from: "vmdata:9000/base-debian13.qcow2"
import_from baut die Platte aus einem vorhandenen Volume, statt ein leeres zuzuteilen — <STORAGE>:<VMID>/<NAME>, oder <STORAGE>:import/<NAME> über das Import-Verzeichnis des Speichers ab PVE 9.x. Es schließt sich mit size gegenseitig aus, und nur root darf absolute Dateisystempfade nutzen.
Der Rest der Parameterliste ist der Grund, Platten mit diesem Modul anzuhängen statt inline im Anlege-Aufruf: cache, aio, iothread, discard, ssd, backup, detect_zeroes, und die ganze Drosselfamilie — iops, iops_rd, iops_wr, ihre _max- und _max_length-Varianten, und die bps_*_max_length-Steuerung für Stöße. Nichts davon ist über proxmox_kvm nach dem Anlegen erreichbar.
Zwei Vorbehalte, beide aus der eigenen Dokumentation des Moduls:
- Manche Optionsänderungen brauchen einen Neustart. „Some updates on options (like
cache) are not being applied instantly and require VM restart.“ Ein grüner Task heißt, dass die Konfiguration geschrieben wurde, nicht dass die laufende VM sich anders verhält. - Es unterstützt den Check-Mode nicht.
check_mode: none, wieproxmox_kvm. Die Collection teilt sich also mitten durch: NIC-Änderungen kann man mit--checküben, Plattenänderungen nicht.
Die Arbeitsteilung
| Um das zu tun | Nimm |
|---|---|
| Die VM anlegen | proxmox_kvm, einmal, an der Existenz getort |
| Kerne, Speicher, Tags, Agent, onboot ändern | proxmox_kvm mit update: true |
| Eine NIC anlegen, umtaggen, trennen oder entfernen | proxmox_nic |
| Eine Platte anlegen, vergrößern, verschieben, abtrennen oder entfernen | proxmox_disk |
| Momentaufnahme | proxmox_snap (auch voller Check-Mode) |
| Alles, was keines davon offenlegt | qm set über SSH |
Eine Platte oder NIC über proxmox_kvm ändern | nichts — dafür ist update_unsafe da, und deshalb solltest du es nicht nutzen |
Bau die VM mit einem knappen proxmox_kvm-Aufruf, dann häng die Platten und Schnittstellen mit ihren eigenen Modulen an. Es sind mehr Tasks, und es ist die Fassung, in der Änderungen am zweiten Tag einen Weg haben, der nicht über eine Option führt, deren dokumentiertes Risiko der Verlust einer Platte ist.
Wo das Modul aufhört
proxmox_kvm hat eine riesige Parameterliste und deckt dennoch nicht alles ab, was qm kann. Statt zu warten, fällt das produktive Playbook auf die Kommandozeile des Knotens zurück:
- 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
Daran ist nichts falsch. Es ist in keinem sinnvollen Sinn idempotent — qm set ist ein Schreiben und wird jeden Lauf changed melden —, aber es ist ausdrücklich, es ist lesbar, und es gibt nicht vor, etwas anderes zu sein. Bekommt ein Modul den Parameter später, löschst du den Task.
Anderes braucht einen zweiten Durchgang durch das Modul mit update: true, weil es nicht im selben Aufruf gesetzt werden kann, der die VM anlegt:
- 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)
Beachte, dass es vmid übergibt, nicht name. Sobald du die ID hast, nutz sie. Sie ist der einzige Bezeichner, den die API als eindeutig behandelt.
Und für die Dinge, die QEMU kann und für die PVE keine Option hat, gibt es args, das wortwörtlich an die QEMU-Kommandozeile weitergegeben wird:
args: >-
-global scsi-hd.physical_block_size=4k
-global scsi-hd.logical_block_size=4096
Das zeigt die virtuelle Platte als 4Kn statt 512e, und das zählt mehr, als es klingt — Blockgrößen, 4Kn und 512e. Das Modul beschriftet args mit „for experts only“, und der Grund ist, dass PVE es nicht prüft und ein falsches Flag die VM nicht starten lässt, mit einem Fehler, der von QEMU kommt und nicht von Proxmox.
Der Cluster ist nicht sofort einig
- name: Let registration complete on cluster
ansible.builtin.pause:
seconds: 5
when: created_vm.changed
Ein pause in einem Playbook riecht meist, und dieses trägt Last. Der Anlege-Aufruf kehrt zurück, wenn die API die Definition angenommen hat, und das ist nicht dasselbe, wie dass jeder Knoten der Existenz der VM zustimmt — und der unmittelbar nächste Task will eine ACL auf /vms/<vmid> setzen. Fünf Sekunden Geduld sind billiger als eine Wiederholschleife um einen Fehler, der nur unter Last auftritt.
Das Abbau-Playbook hat aus demselben Grund dieselbe Form: stoppen, warten, dann löschen.
Zurücklesen, was du gebaut hast
proxmox_kvm dokumentiert drei Rückgabewerte: vmid, status und msg. In der Praxis willst du einen vierten, und der steht nicht in der Dokumentation.
- name: Get MAC address of VM
ansible.builtin.set_fact:
primary_mac_addr: "{{ created_vm.mac.net0 }}"
created_vm.mac ist echt — das Modul baut es in get_vminfo() und klatscht es ins Ergebnis —, aber es fehlt im dokumentierten RETURN-Block, was heißt, dass nichts verspricht, dass es weiter funktioniert. Es ist wert, genau zu wissen, wie es sich verhält, denn es hat zwei Fallstricke:
- Es erscheint nur, wenn das Modul die VM tatsächlich angelegt hat.
macwird nur auf dem Weg des Anlegens und Ausrollens zusammengesetzt. Nimm den Zweig „existiert schon“, und das Ergebnis hatvmidundmsgund nichts weiter. - Es enthält nur die Schnittstellen, die du übergeben hast. Der Code geht die Parameter durch, die du geliefert hast, sucht die heraus, die auf
net[0-9]passen, und liest die gespeicherte Konfiguration jeder einzelnen aus der API zurück. Keinnet-Parameter, keinmac-Schlüssel.
Deshalb braucht das produktive Playbook beide Hälften, und die zweite ist hässlich:
- 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] }}
Existierte die VM schon, gibt es kein mac, die MAC muss also aus der rohen Konfigurationszeichenkette gegraben werden. net0 kommt von der API als virtio=AE:AE:5C:A8:89:85,bridge=vmbr0 zurück, also: an Kommas trennen, das erste Feld nehmen, an = trennen, die zweite Hälfte nehmen. Es ist Zeichenketten-Chirurgie an einer API-Antwort, und es ist der ehrliche Preis eines Moduls, dessen Rückgabeform davon abhängt, welchen Zweig es genommen hat.
Brauchst du die MAC in beiden Fällen verlässlich, hol sie bedingungslos von proxmox_vm_info und wert eine Form aus statt zweier.
Es aus einer verlässlichen Quelle antreiben
Sieh die Zeile am Kopf des Plays noch einmal an:
hosts: "{{ target_hosts | default('cluster_pve:&status_planned') }}"
Das ist die eigentliche Architektur, und es ist klar zu sagen wert: die zu bauenden VMs sind keine Liste in einer Vars-Datei. Sie sind die Hosts in deinem Inventar, deren erfasster Status sagt, dass sie existieren sollen und noch nicht existieren.
Das Inventar hier ist NetBox. Eine VM wird angefordert, indem ein NetBox-Datensatz mit dem Status planned angelegt wird, der CPU, Speicher, Platte, VLAN, Eigentümer und Plattform trägt. Das Playbook wählt die planned-Maschinen aus, baut sie, teilt eine IP zu, schreibt DNS und setzt den Datensatz dann auf staged — und ab dann passt dieser Host nicht mehr auf das Host-Muster des Plays, und ein Handler aktualisiert das Inventar, damit das nächste Play den neuen Zustand sieht:
handlers:
- name: Refresh inventory
ansible.builtin.meta: refresh_inventory
Das Statusfeld ist ein Zustandsautomat, das Playbook ist ein Übergang darin, und das Ganze ist wiederholbar, weil ein Host, der schon weitergegangen ist, nicht mehr ausgewählt wird. Das ist eine viel bessere Eigenschaft als jede Menge Idempotenz auf Modulebene, und es ist der Grund, warum der Anlege-Task damit durchkommt, ein Einmalschuss zu sein.
community.proxmox liefert auch ein eigenes Inventar-Plugin mit, das ein Inventar aus dem Cluster baut — die richtige Wahl, wenn Proxmox die verlässliche Quelle ist. Hier ist es umgekehrt: NetBox ist maßgeblich, und Proxmox ist, wo seine Absicht Wirklichkeit wird. Das ist ein eigener Beitrag, und ich schreibe ihn gesondert.
Es wieder wegnehmen
Anlegen ohne Abbau ist ein halber Lebenszyklus, und der Weg zum Entfernen hat seinen eigenen Fallstrick — eine laufende VM kann man nicht löschen:
- 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 ist ein sanftes Herunterfahren, und das Zusammenspiel mit timeout ist dokumentiert und wert, es sich zu merken: wird die Zeit mit force: true erreicht, wird die VM hart abgeschaltet; mit force: false scheitert der Task stattdessen. Zehn Sekunden sanftes Fenster, gefolgt vom Ziehen des Steckers, ist eine vernünftige Regel für eine Maschine, die abgebaut wird, und eine schlechte für alles andere.
Der volle Abbau (proxmox-remove-vms.yml) wickelt dann den Rest des Datensatzes ab: DNS, die zugeteilte IP, die NetBox-Schnittstellen, die NetBox-VM und die alten Einträge in known_hosts. Es hüllt den Block in ignore_errors: true, was in einem Abbau vertretbar ist. Du entfernst Dinge, die schon weg sein können, und eine halb gelöschte Maschine ist schlimmer als ein lautes Protokoll.
Was ich in einem frischen Aufbau ändern würde
Nachdem ich den Modul-Quellcode gelesen habe und nicht bloß seine Dokumentation, vier Dinge:
- Nimm ein API-Token, nicht
api_userplusapi_password. Eingegrenzt, widerrufbar, und es gehört nie einer Person. - Setz
validate_certsausdrücklich, bevor 2.0.0 es dir unter den Füßen wegzieht. - Teil die VMID selbst zu aus der verlässlichen Quelle. Es beseitigt das Rennen um Lesen-Ändern-Schreiben, lässt dich
serial: 1weglassen und gibt jedem späteren Task einen festen Bezeichner statt eines Namens, der nicht eindeutig ist. - Leg die VM nackt an, dann häng ihre Platten und NICs mit
proxmox_diskundproxmox_nican. Mehr Tasks, aber jede Platte und jede Schnittstelle hat dann ein Modul, das sie später ändern kann — samtcreate: disabledfür Änderungen nur an Optionen undstate: detachedstatt Löschen — und keine Konfiguration, die nur überupdate_unsafezu ändern ist. - Hol die MAC von
proxmox_vm_infoan einer Stelle, damit eine Form auszuwerten ist statt einer Bedingung über einen dokumentierten und einen undokumentierten Rückgabewert.
Und greif auf einem Bau-Playbook nicht nach --check. Das Modul, auf das es ankommt, kann es nicht achten.
Ein Trockenlauf, der immer ja sagt, ist schlimmer als kein Trockenlauf, denn du wirst ihm glauben.
Quellen
- Dokumentation der Collection community.proxmox — die ganze Fläche von 47 Modulen
- community.proxmox auf GitHub — wo der Quellcode von oben liegt;
plugins/modules/proxmox_kvm.pyist die Datei, die man liest, wenn die Dokumentation mehrdeutig ist - Moduldokumentation von
proxmox_kvm— die Parameterliste und die oben zitierten Warnungen zuupdateundupdate_unsafe - Moduldokumentation von
proxmox_vm_info—config: currentundconfig: pending - PVE-Optionsreferenz zu
qm— die eigentliche Spezifikation für jedescsi[n]-,net[n]- undboot-Zeichenkette, die du durchgibst - proxmoxer — der Python-Client, auf dem die Collection aufbaut
- damo2929/ansible-example — die Playbooks, aus denen diese Auszüge kommen, samt NetBox-gestütztem Inventar, DHCP-Erzeugung und Hypervisor-Aufbau