Automatisiertes OS-Deployment mit Proxmox, Ansible und n8n

Einleitung – Ziel des Projekts

In meinem privaten Homelab wollte ich die Bereitstellung neuer Windows-VMs so weit wie möglich automatisieren. Der manuelle Ablauf ist zwar überschaubar, kostet aber trotzdem jedes Mal Zeit: VM anlegen, Template klonen, Ressourcen zuweisen, Netzwerk konfigurieren, Betriebssystem initialisieren, in die Domäne aufnehmen und am Ende die Zugangsdaten weitergeben.

Das Ziel des Projekts war deshalb ein durchgängiger Self-Service-Prozess:

Ein Benutzer beantragt über ein Formular eine neue virtuelle Maschine. Anschließend startet automatisch ein Workflow, der die VM in Proxmox erstellt, per Cloudbase-Init initial konfiguriert, per Ansible weiter vorbereitet und am Ende dem Antragsteller alle relevanten Informationen per E-Mail zusendet.

Für die Umsetzung kommen drei Werkzeuge zusammen:

Proxmox VE als Virtualisierungsplattform,
Ansible für die eigentliche Bereitstellung und Konfiguration,
n8n als Orchestrierungsschicht und Formular-Workflow.

Der große Vorteil dieser Kombination ist, dass klassische Infrastruktur-Aufgaben mit relativ wenig Aufwand in einen wiederverwendbaren und nachvollziehbaren Prozess überführt werden können.

Vorbereitung: Cloudbase-Init und Template in Proxmox

Damit Windows-VMs automatisiert bereitgestellt werden können, braucht es ein sauber vorbereitetes Template. Für Linux würde man hier meist Cloud-Init verwenden, bei Windows übernimmt diese Rolle Cloudbase-Init.

Cloudbase-Init sorgt dafür, dass eine frisch geklonte VM beim ersten Start automatisch initialisiert werden kann. Dazu gehören unter anderem:

  • Setzen des lokalen Benutzers
  • Übernahme eines Passworts
  • Hostname-Konfiguration
  • Netzwerkparameter
  • Verarbeitung der von Proxmox bereitgestellten Cloud-Init-Daten

Installation von Cloudbase-Init

Als Basis dient bei mir eine bereitgestellte Windows VM unter Proxmox. Diese Installation wird direkt im Sysprep-Überwachungsmodus gestartet nach der Installation (im ersten Dialog) mit der Tastenkombination STRG+SHIFT+F3

Anschließend findet die Installation von Cloudbase-Init statt.

Download Cloudbase Init: https://cloudbase.it/cloudbase-init/#download

Nach der Installation sollten noch ein paar typische Vorbereitungen durchgeführt werden:

  • Windows aktualisieren
  • unnötige temporäre Dateien bereinigen
  • gewünschte Basiskonfiguration vornehmen
  • Cloudbase-Init prüfen
  • WinRM aktivieren, damit Ansible später die VM erreichen kann

Gerade der letzte Punkt ist wichtig: Da das Playbook nach dem ersten Boot per WinRM auf die Windows-VM zugreift, muss diese Kommunikation im Template bereits vorbereitet sein.

Anschließend müssen noch einige Dinge vorgenommen werden.

Aktivieren des Administrator-Benutzers

net user Administrator /active:yes

Bearbeiteten der „unattend.xml“

Diese befindet sich im Verzeichnis „C:\Program Files\Cloudbase Solutions\Cloudbase-Init\conf“

Wir bearbeiten die Datei entsprechend dass der lokale Administrator nach dem ersten Start aktiviert wird.

<RunSynchronousCommand wcm:action="add">
  <Path>net user administrator /active:yes</Path>
  <Order>1</Order>
  <Description>Enable Administrator User</Description>
</RunSynchronousCommand>

Wichtig ist dass wir den Befehl mit der <Order>1</Order> an erster Stelle angeben. Die bereits vorhandene Sektion verschieben wir auf <Order>2</Order>

Anpassen der Cloudbase-Config

Wir passen noch die Cloudbase-Konfigurationsdateien entsprechend an. Diese befinden sich im Verzeichnis „C:\Program Files\Cloudbase Solutions\Cloudbase-Init\conf“

cloudbase-init.conf

[DEFAULT]
username=administrator
groups=Administrators
inject_user_password=true
config_drive_raw_hhd=true
config_drive_cdrom=true
config_drive_vfat=true
bsdtar_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exe
mtools_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\
verbose=true
debug=true
log_dir=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\
log_file=cloudbase-init.log
default_log_levels=comtypes=INFO,suds=INFO,iso8601=WARN,requests=WARN
logging_serial_port_settings=
mtu_use_dhcp_config=true
ntp_use_dhcp_config=true
local_scripts_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts\
check_latest_version=true
allow_reboot=true
first_logon_behaviour=no
metadata_services=cloudbaseinit.metadata.services.configdrive.ConfigDriveService

cloudbase-init-unattend.conf

[DEFAULT]
username=administrator
groups=Administrators
inject_user_password=true
config_drive_raw_hhd=true
config_drive_cdrom=true
config_drive_vfat=true
bsdtar_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exe
mtools_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\
verbose=true
debug=true
log_dir=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\
log_file=cloudbase-init-unattend.log
default_log_levels=comtypes=INFO,suds=INFO,iso8601=WARN,requests=WARN
logging_serial_port_settings=
mtu_use_dhcp_config=true
ntp_use_dhcp_config=true
local_scripts_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts\
check_latest_version=false
metadata_services=cloudbaseinit.metadata.services.configdrive.ConfigDriveService
plugins=cloudbaseinit.plugins.common.mtu.MTUPlugin,cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin,cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin
allow_reboot=true
stop_service_on_exit=false
first_logon_behaviour=no

Abschließen der Installation

Sind wir nun fertig mit der Installation können wir das Image abschließen. Wir wechseln dafür mit der Kommandozeile in das Cloudbase Verzeichnis.

cd 'C:\Program Files\Cloudbase Solutions\Cloudbase-Init\conf'

Nun führen wir Sysprep mit folgendem Befehl aus:

C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /unattend:Unattend.xml

Die VM wird nun anschließend heruntergefahren.

Erstellung des Templates in Proxmox

Sobald die Windows-VM vorbereitet ist, wird sie in Proxmox in ein Template umgewandelt. Dieses Template dient anschließend als Gold-Image für alle neuen Instanzen.

Im Playbook wird später genau dieses Template verwendet:

  • Template-VMID: 103
  • Template-Name: template-source

Neue VMs werden dann nicht mehr manuell installiert, sondern direkt aus diesem Template geklont. Das spart Zeit und sorgt für einheitliche Ausgangsstände.

Das Ansible-Playbook im Detail

Das bereitgestellte Playbook besteht aus zwei Plays:

Im ersten Play wird die VM in Proxmox erstellt und vorbereitet.
Im zweiten Play wird auf die frisch erstellte Windows-VM zugegriffen, um sie in die Active-Directory-Domäne aufzunehmen und DNS zu registrieren.

Damit deckt das Playbook bereits einen sehr großen Teil des gesamten Provisionierungsprozesses ab.

Play 1: Klonen und Vorbereiten der Windows-VMs in Proxmox

Der erste Abschnitt läuft auf localhost und verwendet die Collection community.proxmox. Das bedeutet: Ansible spricht direkt mit der Proxmox-API und steuert die VM-Erstellung zentral von der Automatisierungsinstanz aus.

1. Definition der Basisvariablen

Im Variablenblock werden alle zentralen Parameter festgelegt:

  • Proxmox-API-Host, Benutzer und Token
  • Ziel-Node in Proxmox
  • Template-Informationen
  • Storage für VM und Cloud-Init
  • Standardwerte für CPU, RAM und Netzwerk
  • IP-Adressbereich
  • Namensschema für VMs
  • Domäneninformationen für den späteren Join

Besonders interessant ist das Namensschema:

  • Prefix: WIN11-
  • Startnummer: 1

Daraus ergeben sich Hostnamen wie WIN11-01, WIN11-02 usw.

Auch die Netzwerkkonfiguration ist fest vorgegeben. Das Playbook sucht freie IPs im Bereich 192.168.25.200 bis 192.168.25.254 und verwendet dazu vmbr0 als Bridge.

2. Generierung des lokalen Administrator-Passworts

Direkt zu Beginn erzeugt das Playbook ein zufälliges Passwort für den lokalen Windows-Administrator:

- name: Generate Windows admin password
  ansible.builtin.set_fact:
    ci_password: >-
      {{
        ci_password_override
        | default(
            lookup(
              'ansible.builtin.password',
              '/dev/null',
              length=24,
              chars=['ascii_letters', 'digits']
            )
          )
      }}

Wenn kein Passwort explizit übergeben wird, erzeugt Ansible automatisch ein zufälliges 24-stelliges Kennwort. Dieses Passwort wird später in Cloudbase-Init gesetzt und auch für den ersten WinRM-Zugriff verwendet.

Das ist praktisch, weil die Maschine direkt mit individuellen Zugangsdaten bereitgestellt wird.

3. Validierung der Eingabewerte

Bevor überhaupt etwas in Proxmox angelegt wird, prüft das Playbook die Eingangsparameter. Dazu gehören unter anderem:

  • VM-Anzahl größer als 0
  • CPU-Anzahl größer als 0
  • mindestens 1024 MB RAM
  • gültiger IP-Bereich
  • gültige Startnummern

Dadurch werden fehlerhafte Deployments früh abgefangen.

4. Ermitteln vorhandener VMs und belegter Ressourcen

Danach liest das Playbook per proxmox_vm_info den aktuellen VM-Bestand vom Ziel-Node aus. Auf Basis dieser Informationen werden anschließend drei Dinge bestimmt:

  • bereits vergebene VMIDs
  • bereits belegte IP-Adressen
  • bereits verwendete Hostnamen im gewünschten Namensschema

Das ist einer der stärksten Teile des Playbooks: Es arbeitet nicht mit starren Werten, sondern sucht sich automatisch freie Ressourcen.

5. Berechnung freier VMIDs, IPs und Namen

Aus den vorhandenen Daten werden dann Listen mit verfügbaren Werten erzeugt:

  • freie VMIDs ab 500
  • freie IPs im definierten Bereich
  • freie Hostnummern zum Prefix WIN11-

Danach prüft das Playbook jeweils mit assert, ob genug freie Ressourcen verfügbar sind.

Erst wenn das erfolgreich ist, wird weitergemacht.

6. Aufbau der VM-Definitionen

Im nächsten Schritt erzeugt Ansible eine strukturierte Liste aller VMs, die erstellt werden sollen. Pro Eintrag werden unter anderem festgelegt:

  • Name
  • VMID
  • IP-Adresse
  • Hostname

Für eine einzelne VM könnte daraus zum Beispiel werden:

  • Name: WIN11-01
  • VMID: 500
  • IP: 192.168.25.200

Diese Liste dient anschließend als Grundlage für alle weiteren Schleifen.

7. Klonen des Proxmox-Templates

Mit dem Modul community.proxmox.proxmox_kvm wird dann das eigentliche Template geklont:

clone: "{{ proxmox_template_name }}"
vmid: "{{ proxmox_template_vmid }}"
newid: "{{ item.vmid }}"
name: "{{ item.name }}"
full: true

Es wird also ein Full Clone erstellt. Das bedeutet: Die neue VM ist vollständig unabhängig vom Template und kann eigenständig betrieben werden.

8. Konfiguration der geklonten VM

Nach dem Klonen wird die VM weiter konfiguriert. Dazu gehören:

  • CPU-Kerne
  • Arbeitsspeicher
  • Aktivierung des QEMU Guest Agents
  • Einbindung des Cloud-Init-Laufwerks
  • Setzen von Benutzer und Passwort
  • DNS-Server
  • Netzwerkkarte
  • statische IP und Gateway

Besonders relevant ist dieser Teil:

citype: configdrive2
ciuser: "{{ ci_username }}"
cipassword: "{{ ci_password }}"

Damit werden die Informationen an Cloudbase-Init übergeben. Die Windows-VM kann sich beim ersten Start also selbst mit den gewünschten Basisparametern initialisieren.

Auch die statische IP wird direkt in Proxmox gesetzt:

ipconfig0: "ip={{ item.ip }}/{{ vm_netmask }},gw={{ gateway }}"

Das ist ein sauberer Ansatz, weil IP-Konfiguration und VM-Erstellung gemeinsam verwaltet werden.

9. Start der neuen VMs

Sobald alle Parameter gesetzt sind, startet Ansible die erzeugten Maschinen. Ab diesem Punkt übernimmt zunächst Cloudbase-Init im Gastbetriebssystem.

10. Dynamische Aufnahme in das Ansible-Inventar

Einer der elegantesten Schritte folgt direkt danach: Die frisch erzeugten VMs werden per add_host dynamisch in eine neue Inventargruppe aufgenommen:

  • Gruppe: new_windows_vms
  • Verbindung: WinRM
  • Port: 5986
  • Transport: ntlm

Zusätzlich werden auch gleich die Werte für den späteren Domain Join mitgegeben, also zum Beispiel:

  • Domainname
  • OU-Pfad
  • Domain-Admin
  • Domain-Admin-Passwort

Dadurch kann der zweite Play direkt auf diesen Hosts weiterarbeiten, ohne dass ein separates statisches Inventar gepflegt werden muss.

11. Rückgabe der Ergebnisse

Am Ende des ersten Plays gibt das Playbook noch ein Ergebnisobjekt aus. Darin stehen unter anderem:

  • Status
  • generiertes Passwort
  • Anzahl der VMs
  • RAM und vCPUs
  • Definitionen der erzeugten VMs

Genau diese Daten lassen sich später sehr gut an n8n zurückgeben, um daraus automatisch die Benachrichtigungs-E-Mail zu erzeugen.

Play 2: Warten auf Cloudbase-Init und Join in die Domäne

Der zweite Teil des Playbooks arbeitet auf den neu erzeugten Windows-VMs.

1. Warten auf Erreichbarkeit per WinRM

Zuerst wartet Ansible darauf, dass die VM erreichbar ist:

- name: Wait for WinRM
  ansible.builtin.wait_for_connection:
    delay: 30
    sleep: 15
    timeout: 1800

Das ist wichtig, weil frisch gestartete Windows-VMs nach dem Booten oft noch einige Minuten brauchen, bis Cloudbase-Init fertig ist und WinRM sauber antwortet.

2. Join zur Active-Directory-Domäne

Anschließend wird die VM mit microsoft.ad.membership automatisch der AD-Domäne hinzugefügt.

Zusätzlich wird ein Reboot ausgelöst, was für einen Domain Join normal ist.

3. Warten auf den Neustart

Nach dem Reboot wartet das Playbook erneut auf die Erreichbarkeit. Erst wenn der Host wieder online ist, läuft es weiter.

4. Registrierung im DNS

Zum Schluss führt das Playbook noch ipconfig /registerdns aus. Dadurch wird sichergestellt, dass der neue Rechner seinen DNS-Eintrag in der Domänenumgebung sauber registriert.

Das ist ein sinnvoller Abschlussschritt, damit der Host anschließend direkt korrekt auflösbar ist.

Was das Playbook insgesamt leistet

Zusammengefasst automatisiert das Playbook folgende Schritte:

  1. Prüfung der Eingangsparameter
  2. Ermittlung freier VMIDs, IP-Adressen und Namen
  3. Klonen eines Windows-Templates in Proxmox
  4. Konfiguration von CPU, RAM, Netzwerk und Cloud-Init
  5. Start der VM
  6. Übergabe der Maschine an ein dynamisches Ansible-Inventar
  7. Warten auf Cloudbase-Init und WinRM
  8. Domain Join in Active Directory
  9. Neustart und DNS-Registrierung

Damit handelt es sich nicht nur um ein einfaches VM-Provisioning, sondern bereits um ein vollständiges Initial Deployment einer Windows-Arbeitsmaschine oder Server-Instanz.

Kombination mit n8n: Self-Service per Formular

Der letzte Baustein ist n8n. Hier übernimmt n8n die Rolle der Orchestrierung und Benutzerinteraktion.

Die Idee dahinter ist einfach: Der Nutzer soll kein Ansible und kein Proxmox kennen müssen. Stattdessen bekommt er nur ein Formular.

Aufbau des Formulars

Im Formular werden typische Angaben abgefragt:

  • Anzahl vCPUs
  • RAM
  • Anzahl der gewünschten Systeme
  • E-Mail-Adresse des Antragstellers

n8n validiert die Eingaben und bereitet daraus die Variablen für den Ansible-Aufruf vor.

Start des Deployments

Im nächsten Schritt startet n8n das Playbook. Dies wird per „Execute a command“ realisiert.

ANSIBLE_STDOUT_CALLBACK=ansible.posix.json \
ansible-playbook createvm.yml \
-e '{"vm_count":{{ $json["Anzahl VM"] }}, "vm_memory":{{ $json["RAM/ MB"] }}, "vm_cores":{{ $json["vCPUs"] }} }' \
| python3 -c 'import sys,json; d=json.load(sys.stdin); print(json.dumps(next(t["hosts"]["localhost"]["msg"] for p in d["plays"] for t in p["tasks"] if t.get("task",{}).get("name")=="Return result")))' 

Dabei lassen sich Variablen wie vm_count, vm_cores, vm_memory oder auch ein optionales ci_password_override dynamisch an Ansible übergeben.

Rückgabe der Ergebnisse

Anschließend formatiere ich die Ergebnisse mit der Aktion „Code in JavaScript“ noch.

const parsed = JSON.parse($json.stdout);

return [
  {
    json: parsed
  }
];

Automatische Benachrichtigung per Mail

Am Ende des n8n-Workflows erhält der Antragsteller automatisch eine E-Mail mit den wichtigsten Informationen zur neuen VM.

Video des Workflows

In diesem Video zeige ich High-Level den Workflow auf.

Technische Stärken des Ansatzes

Die Lösung hat einige klare Vorteile:

Der gesamte Bereitstellungsprozess ist reproduzierbar und versionierbar. Änderungen am Deployment werden nicht mehr manuell dokumentiert, sondern direkt im Playbook nachvollziehbar gemacht.

Die VM-Namen, IP-Adressen und VMIDs werden automatisch auf freie Werte geprüft. Das reduziert Konflikte und vermeidet viele typische Fehler.

Durch die dynamische Inventarerzeugung kann Ansible unmittelbar nach dem Klonen mit den neuen VMs weiterarbeiten.

Mit Cloudbase-Init und WinRM ist auch Windows sauber in einen modernen Automatisierungsprozess integrierbar.

Und durch n8n wird aus einem technischen Deployment-Skript ein benutzerfreundlicher Self-Service-Workflow.

Fazit

Das Projekt zeigt sehr gut, wie sich mit überschaubarem Aufwand ein automatisiertes Windows-Deployment im Homelab umsetzen lässt. Proxmox übernimmt die Virtualisierung, Cloudbase-Init die Erstinitialisierung, Ansible die technische Provisionierung und n8n die Prozessautomatisierung samt Benutzeroberfläche.

Besonders spannend ist, dass der Prozess nicht beim bloßen VM-Klonen aufhört. Durch die anschließende Aufnahme in die Active-Directory-Domäne und die automatische Rückmeldung an den Antragsteller entsteht ein nahezu vollständiger Bereitstellungsworkflow.

Für ein privates Projekt ist das nicht nur praktisch, sondern auch ein sehr gutes Beispiel dafür, wie sich Infrastructure as Code, Self-Service und klassische Systemadministration miteinander verbinden lassen.

Ansible-Playbook

Hier das entsprechende Ansible Playbook mit Kommentaren.

Dieses ist an die eigene Umgebung anzupassen.


  ---
- name: Clone Windows VMs on Proxmox and prepare inventory
  hosts: localhost
  connection: local
  gather_facts: false
  collections:
    - community.proxmox

  vars:
    ansible_python_interpreter: /path/to/ansible/venv/python

    # -----------------------------
    # Proxmox API configuration
    # Replace these placeholders with your own values.
    # -----------------------------
    proxmox_api_host: "PROXMOX_API_HOST_OR_FQDN"
    proxmox_api_user: "API_USER@REALM"
    proxmox_api_token_id: "API_TOKEN_ID"
    proxmox_api_token_secret: "API_TOKEN_SECRET"
    proxmox_validate_certs: false   # For production, certificate validation should be enabled
    proxmox_node: "PROXMOX_NODE_NAME"

    # -----------------------------
    # Template configuration
    # -----------------------------
    proxmox_template_vmid: 100
    proxmox_template_name: "WINDOWS_TEMPLATE_NAME"

    # -----------------------------
    # Storage configuration
    # -----------------------------
    proxmox_storage: "PROXMOX_STORAGE_NAME"
    proxmox_cloudinit_storage: "PROXMOX_CLOUDINIT_STORAGE_NAME"

    # -----------------------------
    # VM defaults
    # -----------------------------
    vm_name_prefix: "WIN11-"
    vm_name_start: 1
    vmid_start: 500
    vm_count: 1
    vm_cores: 4
    vm_memory: 4096
    vm_bridge: "VM_BRIDGE_NAME"

    # -----------------------------
    # Network configuration
    # Use your own internal network here.
    # Example:
    # vm_ip_base: "192.168.100."
    # dns_server: "192.168.100.10"
    # gateway: "192.168.100.1"
    # -----------------------------
    dns_server: "DNS_SERVER_IP"
    gateway: "GATEWAY_IP"
    vm_ip_base: "VM_NETWORK_BASE."
    vm_ip_start: 200
    vm_ip_end: 254
    vm_netmask: 24

    # -----------------------------
    # Cloudbase-Init credentials
    # The initial local Windows account used for first login / WinRM
    # -----------------------------
    ci_username: "administrator"

    # -----------------------------
    # Active Directory configuration
    # Replace with your own domain settings
    # -----------------------------
    domain_name: "AD_DOMAIN_NAME"
    domain_ou_path: "OU=TARGET_OU,DC=example,DC=local"
    domain_admin_user: "DOMAIN\\JOIN_USER"
    domain_admin_password: "DOMAIN_JOIN_PASSWORD"

  tasks:
    - name: Generate Windows admin password
      ansible.builtin.set_fact:
        ci_password: >-
          {{
            ci_password_override
            | default(
                lookup(
                  'ansible.builtin.password',
                  '/dev/null',
                  length=24,
                  chars=['ascii_letters', 'digits']
                )
              )
          }}

    - name: Validate input
      ansible.builtin.assert:
        that:
          - (vm_count | int) > 0
          - (vm_cores | int) > 0
          - (vm_memory | int) >= 1024
          - (vm_ip_start | int) >= 1
          - (vm_ip_end | int) <= 254
          - (vm_ip_end | int) >= (vm_ip_start | int)
          - (vm_name_start | int) >= 1
        fail_msg: "Invalid VM/network/name parameters"

    - name: Get existing VMs
      community.proxmox.proxmox_vm_info:
        api_host: "{{ proxmox_api_host }}"
        api_user: "{{ proxmox_api_user }}"
        api_token_id: "{{ proxmox_api_token_id }}"
        api_token_secret: "{{ proxmox_api_token_secret }}"
        validate_certs: "{{ proxmox_validate_certs }}"
        node: "{{ proxmox_node }}"
        type: qemu
      register: proxmox_vms

    - name: Extract used VMIDs
      ansible.builtin.set_fact:
        used_vmids: >-
          {{
            proxmox_vms.proxmox_vms
            | default([])
            | map(attribute='vmid')
            | map('int')
            | list
          }}

    - name: Build list of available VMIDs
      ansible.builtin.set_fact:
        available_vmids: >-
          {{
            range((vmid_start | int), ((vmid_start | int) + 1000))
            | list
            | difference(used_vmids | default([]))
            | sort
            | list
          }}

    - name: Ensure enough free VMIDs
      ansible.builtin.assert:
        that:
          - (available_vmids | length) >= (vm_count | int)
        fail_msg: "Not enough free VMIDs available"

    - name: Extract used IP last octets from Proxmox VM configs
      ansible.builtin.set_fact:
        used_ip_octets: >-
          {{
            (
              proxmox_vms.proxmox_vms
              | default([])
              | map(attribute='ipconfig0')
              | select('defined')
              | reject('equalto', None)
              | map('regex_findall', 'ip=' ~ (vm_ip_base | replace('.', '\\.')) ~ '([0-9]+)')
              | list
              | flatten
              | map('int')
              | list
            )
            | unique
            | sort
          }}

    - name: Build list of available IP last octets
      ansible.builtin.set_fact:
        available_ip_octets: >-
          {{
            range((vm_ip_start | int), ((vm_ip_end | int) + 1))
            | list
            | difference(used_ip_octets | default([]))
            | sort
            | list
          }}

    - name: Ensure enough free IPs
      ansible.builtin.assert:
        that:
          - (available_ip_octets | length) >= (vm_count | int)
        fail_msg: "Not enough free IP addresses available in {{ vm_ip_base }}{{ vm_ip_start }}-{{ vm_ip_end }}"

    - name: Extract existing VM names with current prefix
      ansible.builtin.set_fact:
        existing_prefixed_vm_names: >-
          {{
            proxmox_vms.proxmox_vms
            | default([])
            | map(attribute='name')
            | select('defined')
            | reject('equalto', None)
            | select('match', '^' ~ vm_name_prefix | regex_escape ~ '[0-9]+$')
            | list
          }}

    - name: Extract used VM name numbers
      ansible.builtin.set_fact:
        used_name_numbers: >-
          {{
            existing_prefixed_vm_names
            | map('regex_replace', '^' ~ vm_name_prefix | regex_escape, '')
            | map('int')
            | list
            | unique
            | sort
          }}

    - name: Build list of available VM name numbers
      ansible.builtin.set_fact:
        available_name_numbers: >-
          {{
            range((vm_name_start | int), ((vm_name_start | int) + 1000))
            | list
            | difference(used_name_numbers | default([]))
            | sort
            | list
          }}

    - name: Ensure enough free VM names
      ansible.builtin.assert:
        that:
          - (available_name_numbers | length) >= (vm_count | int)
        fail_msg: "Not enough free VM names available for prefix {{ vm_name_prefix }}"

    - name: Build VM definition list
      ansible.builtin.set_fact:
        vm_definitions: >-
          {{
            (vm_definitions | default([])) +
            [{
              'name': vm_name_prefix ~ ('%02d' | format(available_name_numbers[(item | int) - 1])),
              'vmid': available_vmids[(item | int) - 1],
              'ip': vm_ip_base ~ (available_ip_octets[(item | int) - 1] | string),
              'hostname': vm_name_prefix ~ ('%02d' | format(available_name_numbers[(item | int) - 1]))
            }]
          }}
      loop: "{{ range(1, (vm_count | int) + 1) | list }}"

    - name: Show planned VMs
      ansible.builtin.debug:
        msg:
          vm_definitions: "{{ vm_definitions }}"
          used_vmids: "{{ used_vmids }}"
          used_ip_octets: "{{ used_ip_octets }}"
          used_name_numbers: "{{ used_name_numbers }}"
          next_names: "{{ available_name_numbers[0:10] }}"

    - name: Clone template
      community.proxmox.proxmox_kvm:
        api_host: "{{ proxmox_api_host }}"
        api_user: "{{ proxmox_api_user }}"
        api_token_id: "{{ proxmox_api_token_id }}"
        api_token_secret: "{{ proxmox_api_token_secret }}"
        validate_certs: "{{ proxmox_validate_certs }}"
        node: "{{ proxmox_node }}"
        clone: "{{ proxmox_template_name }}"
        vmid: "{{ proxmox_template_vmid }}"
        newid: "{{ item.vmid }}"
        name: "{{ item.name }}"
        storage: "{{ proxmox_storage }}"
        full: true
        timeout: 600
        state: present
      loop: "{{ vm_definitions }}"
      loop_control:
        label: "{{ item.name }}"

    - name: Configure cloned VM
      community.proxmox.proxmox_kvm:
        api_host: "{{ proxmox_api_host }}"
        api_user: "{{ proxmox_api_user }}"
        api_token_id: "{{ proxmox_api_token_id }}"
        api_token_secret: "{{ proxmox_api_token_secret }}"
        validate_certs: "{{ proxmox_validate_certs }}"
        node: "{{ proxmox_node }}"
        vmid: "{{ item.vmid }}"
        name: "{{ item.name }}"
        cores: "{{ vm_cores | int }}"
        memory: "{{ vm_memory | int }}"
        agent: "enabled=1"

        ide:
          ide2: "{{ proxmox_cloudinit_storage }}:cloudinit"

        citype: configdrive2
        ciuser: "{{ ci_username }}"
        cipassword: "{{ ci_password }}"

        nameservers:
          - "{{ dns_server }}"

        net:
          net0: "virtio,bridge={{ vm_bridge }}"

        ipconfig:
          ipconfig0: "ip={{ item.ip }}/{{ vm_netmask }},gw={{ gateway }}"

        update: true
        update_unsafe: true
        state: present
      loop: "{{ vm_definitions }}"
      loop_control:
        label: "{{ item.name }}"

    - name: Start VMs
      community.proxmox.proxmox_kvm:
        api_host: "{{ proxmox_api_host }}"
        api_user: "{{ proxmox_api_user }}"
        api_token_id: "{{ proxmox_api_token_id }}"
        api_token_secret: "{{ proxmox_api_token_secret }}"
        validate_certs: "{{ proxmox_validate_certs }}"
        node: "{{ proxmox_node }}"
        vmid: "{{ item.vmid }}"
        state: started
      loop: "{{ vm_definitions }}"
      loop_control:
        label: "{{ item.name }}"

    - name: Add new VMs to inventory
      ansible.builtin.add_host:
        name: "{{ item.hostname }}"
        groups: new_windows_vms
        ansible_host: "{{ item.ip }}"
        ansible_user: "{{ ci_username }}"
        ansible_password: "{{ ci_password }}"
        ansible_connection: winrm
        ansible_port: 5986
        ansible_winrm_transport: ntlm
        ansible_winrm_server_cert_validation: ignore
        domain_name: "{{ domain_name }}"
        domain_ou_path: "{{ domain_ou_path }}"
        domain_admin_user: "{{ domain_admin_user }}"
        domain_admin_password: "{{ domain_admin_password }}"
      loop: "{{ vm_definitions }}"
      loop_control:
        label: "{{ item.hostname }}"

    - name: Return result
      ansible.builtin.debug:
        msg:
          status: "success"
          password: "{{ ci_password }}"
          vm_count: "{{ vm_count | int }}"
          ram: "{{ vm_memory | int }}"
          vcpus: "{{ vm_cores | int }}"
          vms: "{{ vm_definitions }}"

- name: Wait for Cloudbase-Init and join domain
  hosts: new_windows_vms
  gather_facts: false
  serial: 2
  collections:
    - microsoft.ad
    - ansible.windows

  tasks:
    - name: Wait for WinRM
      ansible.builtin.wait_for_connection:
        delay: 30
        sleep: 15
        timeout: 1800

    - name: Join VM to Active Directory
      microsoft.ad.membership:
        dns_domain_name: "{{ domain_name }}"
        domain_admin_user: "{{ domain_admin_user }}"
        domain_admin_password: "{{ domain_admin_password }}"
        domain_ou_path: "{{ domain_ou_path }}"
        hostname: "{{ inventory_hostname }}"
        state: domain
        reboot: true

    - name: Wait for reboot
      ansible.builtin.wait_for_connection:
        delay: 60
        sleep: 20
        timeout: 1800

    - name: Register DNS
      ansible.windows.win_command: ipconfig /registerdns

Schreibe einen Kommentar