Skip to content

Pflaster drauf – Automatisches Patching von Linux VMs in Proxmox mit Ansible

Wer gerne seine Linux VMs in Proxmox aktuell hält, dass aber von Hand machen muss, der hat ganzschön etwas zu tun. Alle VMs hochfahren, einen Snapshot erstellen, patchen und wieder herunterfahren. Zeit für ein kleines Ansible Playbook, dass uns diese Aufgabe abnimmt.

Gleich vorweg, wer einfach nur mein Playbook nutzen möchte ohne sich jetzt in Ansible und die Proxmox REST API einzuarbeiten, der kann sich das Playbook hier herunterladen und gerne verwenden.

Das Playbook läuft in zwei Phasen ab. Zum einen wird zunächst nur auf dem “localhost” (also dem Ansible Execution Node, dort von wo wir das Playbook starten) ausgeführt. Später, wenn es um das tatsächliche Patchen der VM geht, wird eine Verbindung zu der VM hergestellt. Daraus ergibt sich, dass wir das Ansible Inventory, mit dem wir das Playbook starten entsprechend mit allen VMs befüllen müssen als auch den localhost. Hierbei ist besonders die Groß- und Kleinschreibung zu beachten. Der Name im Inventory und in Proxmox müssen identisch sein. Hier ein Beispiel eines Inventories:

[Patching:vars]       
ansible_user=username
ansible_password='SuperSecretPassword'
ansible_become_user=root
ansible_become_pass='SuperSecretPassword'

[Patching]
vmsrvansibletest1
vmsrvansibletest2

localhost ansible_python_interpreter=/usr/bin/python3

Die Variablen für die Gruppe “Patching” regeln, mit welchem Betriebssystembenutzer und Passwort versucht sich anzumelden. Da die Patchinstallation eine Aufgabe für den root Benutzer ist, müssen wir ebenfalls das sogenannte “Become Passwort” (sudo Passwort) angeben. Alternativ können wir natürlich auch das Playbook mit den Parametern -k und -K aufrufen. Dann werden wir beim Starten des Playbooks nach beiden Passwörtern gefragt.

Nun schauen wir uns das eigentliche Playbook an, dass in der Datei update_proxmox_vm.yml zu finden ist.

---

# Version 1.1.0

- hosts: "localhost"
  vars:
    proxmox_api_host: "pvehost1"
    proxmox_api_user: "root@pam"
    proxmox_api_password: "SuperSecretPassword"
    proxmox_api_port: "8006"
    boot_time: 180
    vm_list:
      - vm_name: myShinyVM1
      - vm_name: myShinyVM2
        snapshot: true

  tasks:

  - name: Logon to Proxmox Server
    uri:
      method: POST
      validate_certs: no
      return_content: yes
      url: "https://{{ proxmox_api_host }}:8006/api2/json/access/ticket"
      body_format: json
      body:
        username: "{{ proxmox_api_user }}"
        password: "{{ proxmox_api_password }}"
    register: proxmox_session
    become: false

  - name: parse cookie data
    set_fact:
      proxmox_api_cookie:
        Cookie: "PVEAuthCookie={{ proxmox_session.json.data.ticket }}"
        CSRFPreventionToken: "{{ proxmox_session.json.data.CSRFPreventionToken }}"
    no_log: true

  - name: get information about all vms in the cluster
    uri:
      method: GET
      validate_certs: no
      url: "https://{{ proxmox_api_host }}:8006/api2/json/cluster/resources?type=vm"
      headers: "{{ proxmox_api_cookie }}"
    register: proxmox_cluster_information

  - name: Patch VM
    include_tasks: patch_vm.yml
    loop: "{{ vm_list }}"

Die Variablen die hier am Anfang definiert sind, müssen auf eure Gegebenheiten angepasst werden. Ihr könnt das entweder tun, indem ihr das Playbook editiert und z.B. den Proxmox Server (proxmox_api_host) und den Proxmox API Benutzer (proxmox_api_user) und dessen Passwort dort eingebt, oder ihr übergebt die Variablen als “Extra Variablen” oder verschlüsselt in einem Ansible Vault. Hier gehen wir davon aus, dass ihr die erforderlichen Informationen im Playbook anpasst.

Weiterhin müsst ihr eine Liste an VMs die ihr patchen wollt in der vordefinierten Liste vm_list angeben. Hier könnt ihr ebenfalls je VM definieren, ob im Rahmen des Patching im Vorfeld ein Snapshot erstellt wird (snapshot: true). In unserem Beispiel würde die Liste so aussehen:

vm_list:
  - vm_name: vmsrvansibletest1
  - vm_name: vmsrvansibletest2
    snapshot: true

Damit würde für die VM “vmsrvansibletest2” ein Snapshot erstellt werden, für die VM “vmsrvansibletest1” jedoch nicht.

Das Play selbst wird damit beginnen, sich an der Proxmox API anzumelden und einen “Access Token” zu bekommen. Anschließend wird eine Liste mit allen VMs (keine LX-Container!) abgefragt. Nun wird ein weiteres “Unter”-Playbook patch_vm.yml aufgerufen und zwar für jede VM in der Liste vm_list.

Das Playbook patch_vm.yml sieht folgendermaßen aus:

---

- name: Update VM
  block:

    - name: Extract Node and VM ID for VM {{ item.vm_name }}
      set_fact:
        vm_id: "{{ proxmox_cluster_information.json.data | selectattr('name', 'equalto', item.vm_name) | map(attribute='vmid') | first }}"
        vm_node: "{{ proxmox_cluster_information.json.data | selectattr('name', 'equalto', item.vm_name) | map(attribute='node') | first }}"
       
    - name: Get VM Status
      uri:
        method: GET
        validate_certs: no
        url: "https://{{ proxmox_api_host }}:{{ proxmox_api_port }}/api2/json/nodes/{{ vm_node }}/qemu/{{ vm_id }}/status/current"
        headers: "{{ proxmox_api_cookie }}"
      register: vm_status
    
    - name: Start VM {{ item.vm_name }}
      uri:
        method: POST
        validate_certs: no
        url: "https://{{ proxmox_api_host }}:{{ proxmox_api_port }}/api2/json/nodes/{{ vm_node }}/qemu/{{ vm_id }}/status/start"
        headers: "{{ proxmox_api_cookie }}"
        body_format: form-urlencoded
        body:
          timeout: "{{ boot_time }}"
      when: vm_status.json.data.status == 'stopped'
    
    - name: Waiting for the VM to boot up
      pause: 
        seconds: "{{ boot_time }}"
      when: vm_status.json.data.status == 'stopped'
    
    - name: Gather VM Facts
      gather_facts:
      register: vm_facts
      delegate_to: "{{ item.vm_name }}"

    - name: Take a VM Snapshot of VM {{ item.vm_name }}
      uri:
        method: POST
        validate_certs: no
        url: "https://{{ proxmox_api_host }}:{{ proxmox_api_port }}/api2/json/nodes/{{ vm_node }}/qemu/{{ vm_id }}/snapshot"
        headers: "{{ proxmox_api_cookie }}"
        body_format: form-urlencoded
        body:
          snapname: "snap_before_patch-{{ ansible_date_time.date }}"
          description: "Snapshot taken by Update Automation"
          vmstate: 1
      when: (item.snapshot|default(false))
    
    - name: Waiting for the VM to finish Snapshot
      pause: 
        seconds: "{{ boot_time }}"
      when: (item.snapshot|default(false))
    
    - name: Update VM {{ item.vm_name }} with apt
      apt:
        force_apt_get: yes
        name: "*"
        state: latest
        update_cache: yes
      become: yes
      delegate_to: "{{ item.vm_name }}"
      when: vm_facts.ansible_facts.ansible_os_family == 'Debian'
    
    - name: Update VM {{ item.vm_name }} with yum
      yum:
        name: "*"
        state: latest
        update_cache: yes
      become: yes
      delegate_to: "{{ item.vm_name }}"
      when: vm_facts.ansible_facts.ansible_os_family == 'RedHat'  
   
    - name: Shutdown VM {{ item.vm_name }} when it was stopped before patching
      uri:
        method: POST
        validate_certs: no
        url: "https://{{ proxmox_api_host }}:{{ proxmox_api_port }}/api2/json/nodes/{{ vm_node }}/qemu/{{ vm_id }}/status/shutdown"
        headers: "{{ proxmox_api_cookie }}"
        body_format: form-urlencoded
        body:
          forceStop: 1
      when: vm_status.json.data.status == 'stopped'

  rescue:
    - debug:
        msg: "There was an Error during Patch Installation on {{ item.vm_name }}. Maybe you misspelled the VM Name. Be aware, that Proxmox VM Names are case sensitive!" 

Mit dem Namen der VM wird nun die VM ID aus Proxmox ausgelesen. Nun wird geprüft, ob die VM überhaupt angeschaltet ist. Diesen Status merken wir uns in der Variable vm_status. Abhängig davon wird die VM gestartet und ein Snapshot durchgeführt (wenn wir das angegeben haben).

Nach dem Hochfahren der VM geht es ans eigentliche Patchen des Betriebssystems. War die VM vor dem Patchen gestoppt, wird sie wieder heruntergefahren und die nächste VM ist an der Reihe. Sollte es irgendwo innerhalb des patch_vm.yml Playbooks zu einem Fehler kommen, wird die VM übersprungen und mit der nächsten VM weitergemacht.

Wenn wir alle Variablen angepasst haben, können wir das Playbook mit folgendem Befehl starten:

 ansible-playbook -i hosts update_proxmox_vm.yml

Wenn wir die Liste der VMs die es zu patchen gilt nicht im Playbook direkt hinterlegen wollen, können wir auch eine Liste als “Extra Variable” in einer Datei angeben. Das sieht dann so aus:

 ansible-playbook -i hosts -e "@vm_list_to_patch.yml" -e '{"proxmox_api_host": "srvoffice2.home.lab", "proxmox_api_password": "Evenmoresecret"}' update_proxmox_vm.yml 

Wenn das Play fehlerfrei funktioniert, hindert uns niemand daran, es regelmäßig auszuführen. Richtig schön wird es, wenn wir das Playbook über einen Scheduler in AWX (z.B. wöchentlich) ausführen. Dann brauchen wir wirklich nie mehr einen Gedanken an das Thema Patchen zu verschwenden.

Philip

6 thoughts on “Pflaster drauf – Automatisches Patching von Linux VMs in Proxmox mit Ansible”

  1. Hallo Philip,

    danke für die tollen Scripts. Ich versuche das ganze über Semaphore anzulegen und scheitere da bei Environment (“Extra variables must be valid JSON”).
    Kannst du da mit einer Anleitung auf die Sprünge helfen?

    Gruß

      1. Ich dachte ich packe die Liste mit den VMs in einer extra Datei (das wäre dann doch das Environment).

        Dementsprechend versuche ich, diesen Block da rein zu packen:

        vm_list:
        – vm_name: vmsrvansibletest1
        – vm_name: vmsrvansibletest2
        snapshot: true

        Dort meckert er dann wegen dem JSON-Format.
        Ich muss an der Stelle erwähnen, dass ich ein absoluter Newbie bin was Ansible angeht. Nutzt du selbst Semaphore?

        Gruß
        Florian

        1. Avatar photo

          Hallo Florian,

          nein, in dem Fall musst du die Liste der VMs als Extra Variablen übergeben. Die Extra Variablen müssen dennoch valides JSON sein (was es bei dir nicht ist).

          Folgendes Beispiel funktioniert als Environment => Extra Vars:


          {
          "boot_time": 60,
          "proxmox_api_host": "192.168.2.123",
          "proxmox_api_user": "root@pam",
          "proxmox_api_password": "Supersecret",
          "vm_list": [
          {
          "vm_name": "srvtest0"
          },
          {
          "vm_name": "srvtest1"
          }
          ]
          }

          Bitte beachte, dass die Liste der VMs case sensitive ist zu den VM Namen, wie du sie in Proxmox vergeben hast UND das Ansible sein Inventory ebenfalls case sensitive verwaltet. Das heißt, die Namen der VMs und Hosts im Inventory müssen identisch sein.

          Gruß Philip

          1. Hallo Philip,

            danke für deine schnelle Antworten.
            Leider komme ich mit Semaphore noch nicht so wirklich klar und beschäftige mich auch zum ersten mal mit Ansible.
            Nutzt du zufällig auch Semaphore oder machst du alles per SSH?
            Falls du Semaphore nutzt, wäre ich sehr an Screenshots interessiert, ich verstehe gerade nur Bahnhof.

            Gerne auch per Mail.

            Gruß
            Florian

            1. Avatar photo

              Hallo Florian,

              ich habe dir soeben eine Mail geschickt mit einer Anleitung (zumindest habe ich es versucht). Um deine Frage zu beantworten, nein, ich verwendet kein Semaphore. Ich verwende AWX für die Verwaltung meiner Ansible Rollen / Playbooks und meines Homelabs. Semaphore halte ich für den Einstieg aber für eine gute Gelegenheit. Kann bei weitem nicht soviel wie AWX, aber ist deutlich einfacher zu handhaben.

              Gruß Philip

    Leave a Reply

    Your email address will not be published. Required fields are marked *