Manchmal möchte ich nach einem Reboot eines PVE Hosts nicht, dass die VMs und LXCs automatisch hochgefahren werden. Ich habe bisher vor so einem Reboot immer den Autostart Haken bei jedem einzelnen LXC und VM entfernen und später wieder setzen müssen.
Das wollte ich unbedingt vereinfachen und gleichzeitig die jeweils aktuellen Settings der einzelnen VMs und LXCs nach Netbox synchronisieren. In diesem Fall dienen tatsächlich die jeweiligen PVE Hosts als “source of truth” für Netbox. Ich habe mir ausserdem das Leben etwas leichter gemacht, indem ich VSCodium (Opensource Alternative zu VSCode von M$) mit zwei praktischen Plugins nachgerüstet habe –> 1. Open Remote - SSH und 2. YAML. Open Remote - SSH macht entfernte Dateisysteme auf dem lokalen Rechner via ssh in VSCodium zugänglich und YAML hilft dabei keine Fehler z.B. bei den Leerzeichen in den Playbooks zu machen. Die Einrichtung erkläre ich hier nicht, das sollte jeder mit dem bereits vorhandenen Material selbst hinkriegen können. VSCodium kann nativ mit git commits umgehen, das macht das Anlegen und V.a. das Ändern von Playbooks wesentlich komfortabler.
An den PVE Hosts die ferngesteuert und abgefragt werden sollen, müssen noch API Token generiert werden. Bei mir zuhause sind das zwei PVE Hosts, jeweils standalone. Ich muss also zwei API Token generieren. Der Token wird nur einmal, direkt nach dem Erzeugen angezeigt, es empfiehlt sich daher den Token an einem sicheren Ort (Passwortmanager) zu speichern.
Bei den Playbooks habe ich ein neues yml File angelegt mit folgendem Inhalt:
---
- name: NetBox VM-Config gegen PVE abgleichen (Standort_A)
hosts: localhost
connection: local
gather_facts: false
vars:
netbox_api: "https://192.168.2.10"
netbox_token: "{{ lookup('env', 'NETBOX_TOKEN') }}"
# ACHTUNG: Slug, steht bei Standort und ist nur in Bearbeitung sichtbnar
target_site_slug: "standort-a"
pve_nodes:
- name: pve01
api: "https://192.168.2.16:8006/api2/json"
token_id: "{{ lookup('env', 'PVE01_TOKEN_ID') }}"
token_secret: "{{ lookup('env', 'PVE01_TOKEN_SECRET') }}"
- name: pve03
api: "https://192.168.2.18:8006/api2/json"
token_id: "{{ lookup('env', 'PVE03_TOKEN_ID') }}"
token_secret: "{{ lookup('env', 'PVE03_TOKEN_SECRET') }}"
dry_run: false
start_on_boot_enabled_value: "on"
start_on_boot_disabled_value: "off"
start_on_boot_laststate_value: "last-state"
resource_drift: []
onboot_actions: []
netbox_update_actions: []
tasks:
# -----------------------------------------------------------------
# 1. NetBox-VMs der Ziel-Site holen und auf vmid pruefen
# -----------------------------------------------------------------
- name: NetBox-VMs der Ziel-Site holen
ansible.builtin.set_fact:
netbox_vms: >-
{{ query('netbox.netbox.nb_lookup', 'virtual-machines',
api_endpoint=netbox_api,
token=netbox_token,
api_filter='site=' ~ target_site_slug,
validate_certs=false)
| map(attribute='value')
| list }}
- name: Anzahl gefundener NetBox-VMs
ansible.builtin.debug:
msg: "{{ netbox_vms | length }} VMs in NetBox fuer Site '{{ target_site_slug }}' gefunden"
- name: Nur VMs mit gepflegter vmid behalten
ansible.builtin.set_fact:
netbox_vms_ready: >-
{{ netbox_vms
| selectattr('custom_fields.vmid', 'defined')
| selectattr('custom_fields.vmid', 'ne', None)
| list }}
netbox_vms_incomplete: >-
{{ netbox_vms
| rejectattr('custom_fields.vmid', 'defined')
| list
+ netbox_vms | selectattr('custom_fields.vmid', 'defined')
| selectattr('custom_fields.vmid', 'equalto', None) | list }}
- name: "WARNUNG - VMs ohne vmid in NetBox (werden uebersprungen)"
ansible.builtin.debug:
msg: "'{{ item.name }}': vmid={{ item.custom_fields.vmid | default('FEHLT') }}"
loop: "{{ netbox_vms_incomplete }}"
loop_control:
label: "{{ item.name }}"
# -----------------------------------------------------------------
# 2. PVE-Ressourcen von BEIDEN Standalone-Nodes holen, zusammenfuehren
# und nach vmid indexieren (liefert vmid, name, node, maxcpu,
# maxmem, maxdisk - reicht fuer node-Aufloesung UND den
# vCPU/RAM/Disk-Diff)
# -----------------------------------------------------------------
- name: Ressourcen-Stand von JEDEM PVE-Node einzeln holen (Standalone, kein Proxmox-Cluster)
ansible.builtin.uri:
url: "{{ item.api }}/cluster/resources?type=vm"
method: GET
headers:
Authorization: "PVEAPIToken={{ item.token_id }}={{ item.token_secret }}"
validate_certs: false
status_code: [200]
loop: "{{ pve_nodes }}"
loop_control:
label: "{{ item.name }}"
register: pve_resources_raw
no_log: true
- name: Ergebnisse aller Nodes zu einer Liste zusammenfuehren
ansible.builtin.set_fact:
pve_resources_all: >-
{{ pve_resources_raw.results
| map(attribute='json')
| map(attribute='data')
| flatten(levels=1) }}
- name: PVE-Node-Name zu vollen Node-Infos (API-URL + Token) Lookup bauen
ansible.builtin.set_fact:
pve_node_lookup_helper: "{{ dict(pve_nodes | map(attribute='name') | list | zip(pve_nodes)) }}"
- name: "PVE-Ressourcen (LXC UND QEMU) nach vmid indexieren (inkl. API-URL + Token + Typ des jeweiligen Nodes)"
ansible.builtin.set_fact:
pve_by_vmid: "{{ pve_by_vmid | default({}) | combine({ (item.vmid | string): (item | combine({
'api': pve_node_lookup_helper[item.node].api,
'token_id': pve_node_lookup_helper[item.node].token_id,
'token_secret': pve_node_lookup_helper[item.node].token_secret
})) }) }}"
loop: "{{ pve_resources_all | selectattr('type', 'in', ['lxc', 'qemu']) | list }}"
loop_control:
label: "{{ item.name }}"
- name: "WARNUNG - NetBox-vmid ohne Treffer in PVE (werden uebersprungen)"
ansible.builtin.debug:
msg: "'{{ item.name }}' (vmid={{ item.custom_fields.vmid }}) nicht in PVE gefunden"
loop: "{{ netbox_vms_ready }}"
loop_control:
label: "{{ item.name }}"
when: (item.custom_fields.vmid | string) not in pve_by_vmid
- name: Nur tatsaechlich in PVE vorhandene VMs weiterverarbeiten
ansible.builtin.set_fact:
netbox_vms_matched: >-
{{ netbox_vms_ready
| selectattr('custom_fields.vmid', 'in', pve_by_vmid.keys() | map('int') | list)
| list }}
# -----------------------------------------------------------------
# 3. vCPU/RAM/Disk-Drift anzeigen (NUR Anzeige, noch kein Push)
# -----------------------------------------------------------------
- name: Ressourcen-Drift bauen (vCPU/RAM/Disk, Diff-Anzeige only)
ansible.builtin.set_fact:
resource_drift: "{{ resource_drift | default([]) + [{
'name': item.name,
'netbox_id': item.id,
'vmid': item.custom_fields.vmid,
'node': pve_by_vmid[item.custom_fields.vmid | string].node,
'netbox_vcpus': item.vcpus | default(None),
'pve_vcpus': pve_by_vmid[item.custom_fields.vmid | string].maxcpu | int,
'netbox_memory_mb': item.memory | default(None),
'pve_memory_mb': (pve_by_vmid[item.custom_fields.vmid | string].maxmem | int / 1024 / 1024) | round(0, 'common') | int,
'netbox_disk_mb': item.disk | default(None),
'pve_disk_mb': (pve_by_vmid[item.custom_fields.vmid | string].maxdisk | int / 1024 / 1024) | round(0, 'common') | int
}] }}"
loop: "{{ netbox_vms_matched }}"
loop_control:
label: "{{ item.name }}"
- name: "INFO - Ressourcen-Drift je VM (Anzeige; Reverse-Push PVE->NetBox folgt darunter)"
ansible.builtin.debug:
msg: >-
{{ item.name }} (vmid={{ item.vmid }}, node={{ item.node }}):
vCPU NetBox={{ item.netbox_vcpus }} PVE={{ item.pve_vcpus }}
{{ '[ABWEICHUNG]' if item.netbox_vcpus | int != item.pve_vcpus else '' }} |
RAM(MB) NetBox={{ item.netbox_memory_mb }} PVE={{ item.pve_memory_mb }}
{{ '[ABWEICHUNG]' if item.netbox_memory_mb | int != item.pve_memory_mb else '' }} |
Disk(MB) NetBox={{ item.netbox_disk_mb }} PVE={{ item.pve_disk_mb }}
{{ '[ABWEICHUNG - SHRINK, wird NICHT gepusht]' if item.netbox_disk_mb | int < item.pve_disk_mb
else ('[ABWEICHUNG - GROW]' if item.netbox_disk_mb | int > item.pve_disk_mb else '') }}
loop: "{{ resource_drift }}"
loop_control:
label: "{{ item.name }}"
- name: NetBox-Update-Aktionsplan bauen (PVE-Ist-Werte sollen nach NetBox uebernommen werden)
ansible.builtin.set_fact:
netbox_update_actions: "{{ netbox_update_actions | default([]) + [{
'name': item.name,
'netbox_id': item.netbox_id,
'vmid': item.vmid,
'target_vcpus': item.pve_vcpus,
'target_memory': item.pve_memory_mb,
'target_disk': item.pve_disk_mb,
'any_change': (item.netbox_vcpus | int != item.pve_vcpus)
or (item.netbox_memory_mb | int != item.pve_memory_mb)
or (item.netbox_disk_mb | int != item.pve_disk_mb)
}] }}"
loop: "{{ resource_drift }}"
loop_control:
label: "{{ item.name }}"
- name: "UEBERSICHT - NetBox-Objekte, die auf PVE-Ist-Werte aktualisiert werden"
ansible.builtin.debug:
msg: "'{{ item.name }}' (netbox_id={{ item.netbox_id }}): vCPU->{{ item.target_vcpus }} RAM->{{ item.target_memory }}MB Disk->{{ item.target_disk }}MB"
loop: "{{ netbox_update_actions }}"
loop_control:
label: "{{ item.name }}"
when: item.any_change
- name: "PVE-Ist-Werte nach NetBox schreiben (NUR wenn dry_run=false, NUR bei Abweichung)"
ansible.builtin.uri:
url: "{{ netbox_api }}/api/virtualization/virtual-machines/{{ item.netbox_id }}/"
method: PATCH
headers:
Authorization: "Token {{ netbox_token }}"
body_format: json
body:
vcpus: "{{ item.target_vcpus }}"
memory: "{{ item.target_memory }}"
disk: "{{ item.target_disk }}"
validate_certs: false
status_code: [200]
loop: "{{ netbox_update_actions }}"
loop_control:
label: "{{ item.name }}"
when:
- not (dry_run | bool)
- item.any_change
register: netbox_update_result
failed_when: false
- name: Fehlgeschlagene NetBox-Updates auflisten
ansible.builtin.debug:
msg: "FEHLER bei {{ item.item.name }}: {{ item.msg | default('unbekannt') }}"
loop: "{{ netbox_update_result.results | default([]) }}"
loop_control:
label: "{{ item.item.name }}"
when:
- item.item is defined
- item.status is defined
- item.status != 200
# -----------------------------------------------------------------
# 4. onboot / start_on_boot: vollstaendig implementiert, inkl. Push
# -----------------------------------------------------------------
- name: "'Letzter Status' VMs herausfiltern (werden bewusst nicht gepusht)"
ansible.builtin.set_fact:
netbox_vms_laststate: "{{ netbox_vms_matched | selectattr('start_on_boot.value', 'equalto', start_on_boot_laststate_value) | list }}"
netbox_vms_pushable: "{{ netbox_vms_matched | rejectattr('start_on_boot.value', 'equalto', start_on_boot_laststate_value) | list }}"
- name: "INFO - VMs mit 'Letzter Status' (nicht abbildbar in Proxmox, wird uebersprungen)"
ansible.builtin.debug:
msg: "'{{ item.name }}' (vmid={{ item.custom_fields.vmid }}) steht auf 'Letzter Status' - wird nicht angefasst"
loop: "{{ netbox_vms_laststate }}"
loop_control:
label: "{{ item.name }}"
- name: Aktuelle PVE-Config je gematchter, pushbarer VM/LXC holen (fuer onboot-Wert)
ansible.builtin.uri:
url: "{{ pve_by_vmid[item.custom_fields.vmid | string].api }}/nodes/{{ pve_by_vmid[item.custom_fields.vmid | string].node }}/{{ pve_by_vmid[item.custom_fields.vmid | string].type }}/{{ item.custom_fields.vmid }}/config"
method: GET
headers:
Authorization: "PVEAPIToken={{ pve_by_vmid[item.custom_fields.vmid | string].token_id }}={{ pve_by_vmid[item.custom_fields.vmid | string].token_secret }}"
validate_certs: false
status_code: [200]
loop: "{{ netbox_vms_pushable }}"
loop_control:
label: "{{ item.name }}"
register: pve_configs_raw
- name: Aktionsplan bauen (Diff NetBox start_on_boot vs. PVE onboot)
ansible.builtin.set_fact:
onboot_actions: "{{ onboot_actions | default([]) + [{
'name': item.item.name,
'vmid': item.item.custom_fields.vmid,
'node': pve_by_vmid[item.item.custom_fields.vmid | string].node,
'type': pve_by_vmid[item.item.custom_fields.vmid | string].type,
'api': pve_by_vmid[item.item.custom_fields.vmid | string].api,
'token_id': pve_by_vmid[item.item.custom_fields.vmid | string].token_id,
'token_secret': pve_by_vmid[item.item.custom_fields.vmid | string].token_secret,
'netbox_start_on_boot': item.item.start_on_boot.value,
'netbox_wants_enabled': (item.item.start_on_boot.value == start_on_boot_enabled_value),
'pve_onboot': (item.json.data.onboot | default(0) | int == 1)
}] }}"
loop: "{{ pve_configs_raw.results }}"
loop_control:
label: "{{ item.item.name }}"
- name: "DRY RUN / UEBERSICHT - Alle gematchten, pushbaren VMs mit onboot-Stand"
ansible.builtin.debug:
msg: >-
{{ item.name }} (vmid={{ item.vmid }}, node={{ item.node }}):
NetBox={{ item.netbox_start_on_boot }} ({{ 'on' if item.netbox_wants_enabled else 'off' }})
PVE-onboot={{ item.pve_onboot }}
{{ '[WIRD GEAENDERT]' if item.netbox_wants_enabled != item.pve_onboot else '[unveraendert]' }}
loop: "{{ onboot_actions }}"
loop_control:
label: "{{ item.name }}"
- name: onboot-Wert in PVE setzen (nur bei tatsaechlicher Abweichung, nur wenn dry_run=false)
ansible.builtin.uri:
url: "{{ item.api }}/nodes/{{ item.node }}/{{ item.type }}/{{ item.vmid }}/config"
method: PUT
headers:
Authorization: "PVEAPIToken={{ item.token_id }}={{ item.token_secret }}"
body_format: form-urlencoded
body:
onboot: "{{ 1 if item.netbox_wants_enabled else 0 }}"
validate_certs: false
status_code: [200]
loop: "{{ onboot_actions }}"
loop_control:
label: "{{ item.name }}"
when:
- not (dry_run | bool)
- item.netbox_wants_enabled != item.pve_onboot
register: onboot_push_result
failed_when: false
- name: Fehlgeschlagene onboot-Updates auflisten
ansible.builtin.debug:
msg: "FEHLER bei {{ item.item.name }}: {{ item.msg | default('unbekannt') }}"
loop: "{{ onboot_push_result.results | default([]) }}"
loop_control:
label: "{{ item.item.name }}"
when:
- item.item is defined
- item.status is defined
- item.status != 200
den “slug” sieht man am Standort wenn man auf Bearbeiten klickt. Da ich meinen Wohnrt nicht im Internet teilen möchte, habe ich die Felder verpixelt. In meinem Fall sind zwei PVE Hosts vorhanden und deren beiden IPs sind hart in das Skript kodiert. Wer das Skript verwenden möchte muss es an die eigene Situation anpassen.
Aus Faulheitsgründen habe ich die vorher erzeugten PVE Token einfach in das Variablenfile der dns-push skripte dazugepackt. Man könnte das natürlich auch trennen, hat beides Vor- und Nachteile…
Dem bereits vorhandenen Netbox Token habe ich nachträglich noch Scheibrechte eingeräumt (geht nachträglich über die GUI), sonst ließen sich keine Werte (vCPU, RAM etc.) aus PVE in Netbox schreiben. Zum Schluss noch das neue Playbook via Template in Semaphore einfügen.
Die virtuellen Maschinen kennen bei Netbox bereits ein Feld für den Zustand nach Reboot
Bei PVE wird als “Primary Key” zur Identifikation die sogenannte VMID verwendet. Dafür gibt es (noch) kein Feld bei Netbox. Nachdem man das neue Feld erzeugt hat, muss man es noch für jede virtuelle Instanz manuell befüllen. Nur LXCs und VMs die in Netbox manuell eine solche vmid zugewiesen bekommen haben, werden von obigen Skript auch berücksichtigt, der Rest wird ignoriert.
Nachdem ich das Skript ein paar mal getestet hatte, habe ich einen cronjob alle 10 Minuten dafür eingerichtet, das geht in Semaphore ja auch über die GUI. Ich kann jetzt zentral in Netbox das Verhalten aller virtuellen Instanzen nach einem Reboot des jeweiligen PVE Hosts steuern. Im gleichen Schritt werden die Werte für vCPU, RAM etc von den virtuellen Instanzen ausgelesen und in Netbox synchronisiert. Netbox tut sich etwas schwer mit der Umrechnung (Bei Netbox sind 1000MB ein GB und nicht 1024MB wie es richtig wäre), das stört mich aber nicht weiter. Der große Vorteil dabei ist nun, dass ich das Rebootverhalten nun auch massenhaft ändern kann und ich mich nicht mehr einzeln durchklicken muss.







