--- # Provision Windows Server VM on Hyper-V # This playbook creates a new Windows Server VM with unattended installation # # Variables (can be provided via survey in AAP or command line): # vm_name: Name of the VM to create (required) # vm_ip_address: Static IP address for the VM (optional) # vm_cpu_count: Number of CPUs (default: from group_vars) # vm_memory_gb: Memory in GB (default: from group_vars) # vm_disk_size_gb: Disk size in GB (default: from group_vars) # vm_admin_password: Initial administrator password (default: P@ssw0rd123!) # # Tags: # - create: Create VM and configuration # - install: Start VM and wait for installation # - verify: Verify VM is accessible # # Usage: # ansible-playbook provision-vm.yml -e vm_name=WEB01 -e vm_ip_address=192.168.1.101 # ansible-playbook provision-vm.yml --tags create -e vm_name=WEB01 - name: Provision Windows Server VM on Hyper-V hosts: hyperv gather_facts: false vars: # Derived paths vm_path: "{{ vm_storage_path }}\\{{ vm_name }}" vm_vhd_path: "{{ vm_storage_path }}\\{{ vm_name }}\\{{ vm_name }}.vhdx" autounattend_path: "{{ vm_storage_path }}\\{{ vm_name }}\\autounattend.xml" pre_tasks: - name: Set VM configuration with defaults ansible.builtin.set_fact: vm_cpu_count: "{{ vm_cpu_count | default(default_vm_cpu_count) }}" vm_memory_gb: "{{ vm_memory_gb | default(default_vm_memory_gb) }}" vm_disk_size_gb: "{{ vm_disk_size_gb | default(default_vm_disk_size_gb) }}" vm_switch: "{{ vm_switch | default(default_vm_switch) }}" - name: Get available Hyper-V virtual switches ansible.windows.win_shell: | Get-VMSwitch | Select-Object Name, SwitchType | ConvertTo-Json register: available_switches changed_when: false tags: [create, verify] - name: Parse available switches ansible.builtin.set_fact: switch_list: "{{ available_switches.stdout | from_json }}" when: available_switches.stdout | trim | length > 0 tags: [create, verify] - name: Display available switches ansible.builtin.debug: msg: - "Available Hyper-V switches:" - "{{ switch_list | default([]) | map(attribute='Name') | list }}" - "" - "Configured switch: {{ vm_switch }}" tags: [create, verify] - name: Validate virtual switch exists ansible.builtin.assert: that: - switch_list is defined - switch_list | selectattr('Name', 'equalto', vm_switch) | list | length > 0 fail_msg: | Virtual switch '{{ vm_switch }}' not found on Hyper-V host. Available switches: {{ switch_list | default([]) | map(attribute='Name') | list | join(', ') }} To fix this: 1. Update the vm_switch variable: -e vm_switch="" 2. Or update default in group_vars/hyperv/vars.yml 3. Or create the switch on Hyper-V host: New-VMSwitch -Name "{{ vm_switch }}" -SwitchType Internal success_msg: "Virtual switch '{{ vm_switch }}' is available" tags: [create] - name: Validate required variables ansible.builtin.assert: that: - vm_name is defined - vm_name | length > 0 - vm_name is match('^[a-zA-Z0-9-]+$') fail_msg: "vm_name is required and must contain only letters, numbers, and hyphens" tasks: - name: Check if VM already exists ansible.windows.win_shell: | Get-VM -Name "{{ vm_name }}" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name register: vm_exists_check changed_when: false failed_when: false tags: [create, verify] - name: Set VM exists fact ansible.builtin.set_fact: vm_exists: "{{ vm_exists_check.stdout | trim | length > 0 }}" tags: [create, verify] - name: Display VM status ansible.builtin.debug: msg: "VM {{ vm_name }} {{ 'already exists' if vm_exists else 'will be created' }}" tags: [create, verify] - name: VM Creation Block when: not vm_exists tags: [create] block: - name: Create VM directory ansible.windows.win_file: path: "{{ vm_path }}" state: directory - name: Generate autounattend.xml ansible.windows.win_template: src: ../templates/autounattend.xml.j2 dest: "{{ autounattend_path }}" - name: Create new VHD ansible.windows.win_shell: | New-VHD -Path "{{ vm_vhd_path }}" ` -SizeBytes {{ vm_disk_size_gb }}GB ` -Dynamic args: creates: "{{ vm_vhd_path }}" - name: Create new VM ansible.windows.win_shell: | New-VM -Name "{{ vm_name }}" ` -MemoryStartupBytes {{ vm_memory_gb }}GB ` -Generation 2 ` -VHDPath "{{ vm_vhd_path }}" ` -SwitchName "{{ vm_switch }}" register: vm_create - name: Configure VM processor count ansible.windows.win_shell: | Set-VMProcessor -VMName "{{ vm_name }}" -Count {{ vm_cpu_count }} - name: Enable dynamic memory ansible.windows.win_shell: | Set-VMMemory -VMName "{{ vm_name }}" ` -DynamicMemoryEnabled $true ` -MinimumBytes 2GB ` -MaximumBytes {{ vm_memory_gb }}GB - name: Disable secure boot (for compatibility) ansible.windows.win_shell: | Set-VMFirmware -VMName "{{ vm_name }}" -EnableSecureBoot Off - name: Create DVD drive for ISO ansible.windows.win_shell: | Add-VMDvdDrive -VMName "{{ vm_name }}" -Path "{{ windows_server_iso }}" - name: Set DVD as first boot device ansible.windows.win_shell: | $dvd = Get-VMDvdDrive -VMName "{{ vm_name }}" Set-VMFirmware -VMName "{{ vm_name }}" -FirstBootDevice $dvd - name: Create VFD for autounattend.xml ansible.windows.win_shell: | # Create a temporary VFD file $vfdPath = "{{ vm_path }}\autounattend.vfd" # PowerShell to create VFD and add autounattend.xml # Note: This is a simplified approach # For production, consider using a pre-built VFD or ISO with autounattend.xml # Copy autounattend.xml to a location accessible during Windows Setup # Alternative: Use NoCloud data source or inject into ISO Write-Host "AutoUnattend.xml created at {{ autounattend_path }}" Write-Host "Manual step: Mount autounattend.xml to VM or inject into ISO" register: vfd_create changed_when: false - name: Display next steps for autounattend.xml ansible.builtin.debug: msg: - "AutoUnattend.xml has been generated at: {{ autounattend_path }}" - "To use it, manually:" - "1. Create an ISO with autounattend.xml in the root" - "2. Or copy it to a floppy image and attach to VM" - "3. Or use UEFI boot with the file in the EFI partition" - "" - "For automated approach, consider:" - "- Creating a custom Windows ISO with autounattend.xml embedded" - "- Using MDT/WDS for network-based deployment" - name: VM Installation Block when: not vm_exists tags: [install] block: - name: Start VM for installation ansible.windows.win_shell: | Start-VM -Name "{{ vm_name }}" register: vm_start - name: Wait for VM to start ansible.builtin.pause: seconds: 30 - name: Display installation progress message ansible.builtin.debug: msg: - "VM {{ vm_name }} has been started" - "Windows Server installation is in progress..." - "This may take 15-30 minutes depending on hardware" - "" - "Installation steps:" - "1. Windows Setup will boot from ISO" - "2. AutoUnattend.xml will configure installation (if properly mounted)" - "3. System will reboot after installation" - "4. First logon commands will configure WinRM" - name: Wait for Windows installation to complete ansible.builtin.pause: prompt: | Manual step required: 1. Monitor the VM installation through Hyper-V Manager 2. Verify autounattend.xml is being used (watch for automated setup) 3. Wait for installation to complete and system to reach login screen 4. Verify WinRM is enabled (first logon commands should run) Press Enter when installation is complete and VM is at login screen... when: ansible_check_mode is not defined or not ansible_check_mode - name: VM Verification Block tags: [verify] block: - name: Get VM state ansible.windows.win_shell: | Get-VM -Name "{{ vm_name }}" | Select-Object Name, State, CPUUsage, MemoryAssigned, Uptime | ConvertTo-Json register: vm_state changed_when: false - name: Display VM state ansible.builtin.debug: var: vm_state.stdout | from_json - name: Wait for WinRM to be available on new VM ansible.builtin.wait_for: host: "{{ vm_ip_address }}" port: 5985 timeout: 600 state: started when: - vm_ip_address is defined - not vm_exists ignore_errors: true - name: Test WinRM connectivity ansible.builtin.command: cmd: > ansible {{ vm_name }} -m ansible.windows.win_ping -i {{ vm_ip_address }}, delegate_to: localhost when: - vm_ip_address is defined register: winrm_test failed_when: false changed_when: false - name: Display connectivity test result ansible.builtin.debug: msg: "{{ 'WinRM connectivity successful' if winrm_test.rc == 0 else 'WinRM connectivity failed - manual verification needed' }}" when: vm_ip_address is defined - name: Post-provisioning tasks when: not vm_exists tags: [create] block: - name: Summary message ansible.builtin.debug: msg: - "=========================================" - "VM Provisioning Summary" - "=========================================" - "VM Name: {{ vm_name }}" - "CPU Count: {{ vm_cpu_count }}" - "Memory: {{ vm_memory_gb }} GB" - "Disk Size: {{ vm_disk_size_gb }} GB" - "IP Address: {{ vm_ip_address | default('DHCP') }}" - "VHD Path: {{ vm_vhd_path }}" - "AutoUnattend: {{ autounattend_path }}" - "=========================================" - "" - "Next Steps:" - "1. Verify VM installation completed successfully" - "2. Test WinRM connectivity: ansible {{ vm_name }} -m win_ping" - "3. Add VM to inventory if using static groups" - "4. Run baseline configuration playbook" - "5. Deploy applications as needed" - "" - "To add to inventory, update:" - "/home/ptoal/Dev/inventories/toallab-inventory/static.yml" - name: Create inventory addition snippet ansible.builtin.set_fact: inventory_snippet: | # Add this to /home/ptoal/Dev/inventories/toallab-inventory/static.yml # Under the appropriate group (web_servers, app_servers, db_servers): {{ vm_name }}: ansible_host: {{ vm_ip_address | default('SET_IP_HERE') }} - name: Display inventory addition snippet ansible.builtin.debug: var: inventory_snippet