From 171708155057bf2dc7a8613f1228487023df64a0 Mon Sep 17 00:00:00 2001 From: Patrick Toal Date: Tue, 28 Apr 2026 22:45:25 -0400 Subject: [PATCH] Create provisioning --- README.md | 117 +++++++-- ansible.cfg | 13 - playbooks/README-provision.md | 345 ++++++++++++++++++++++++++ playbooks/create-autounattend-iso.yml | 93 +++++++ playbooks/provision-vm.yml | 283 +++++++++++++++++++-- templates/autounattend.xml.j2 | 158 ++++++++++++ 6 files changed, 965 insertions(+), 44 deletions(-) create mode 100644 playbooks/README-provision.md create mode 100644 playbooks/create-autounattend-iso.yml create mode 100644 templates/autounattend.xml.j2 diff --git a/README.md b/README.md index 23247c5..e9f120f 100644 --- a/README.md +++ b/README.md @@ -14,37 +14,74 @@ This project demonstrates automated Windows Server VM management including: ## Quick Start +### Development Environment + ```bash +# Activate Ansible virtual environment +source ~/.venv/ansible/bin/activate + # Install required collections ansible-galaxy collection install -r collections/requirements.yml -# Run VM provisioning -ansible-playbook playbooks/provision-vm.yml -e vm_name=DEMO-WEB01 +# Verify Hyper-V connectivity +ansible hyperv -m ansible.windows.win_ping +``` +### Provision a New VM + +```bash +# Using ansible-navigator (recommended) +ansible-navigator run playbooks/provision-vm.yml \ + --execution-environment-image aap.toal.ca/ee-demo \ + --extra-vars "vm_name=WEB01 vm_ip_address=192.168.1.101" + +# Or with ansible-playbook +ansible-playbook playbooks/provision-vm.yml \ + -e vm_name=WEB01 \ + -e vm_ip_address=192.168.1.101 \ + -e vm_cpu_count=4 \ + -e vm_memory_gb=8 +``` + +**See [playbooks/README-provision.md](playbooks/README-provision.md) for detailed provisioning guide** + +### Manage Existing VMs + +```bash # Patch management ansible-playbook playbooks/patch-vms.yml --limit windows_servers # Install IIS demo application ansible-playbook playbooks/install-iis.yml --limit web_servers + +# Update CMDB +ansible-playbook playbooks/sync-cmdb.yml ``` ## Project Structure ``` . -├── ansible.cfg # Ansible configuration -├── playbooks/ # Playbooks by use case -│ ├── provision-vm.yml -│ ├── patch-vms.yml -│ ├── install-iis.yml -│ └── sync-cmdb.yml -├── roles/ # Custom roles -├── group_vars/ # Group variables -├── host_vars/ # Host-specific variables -└── collections/ # Ansible collections - └── requirements.yml +├── ansible.cfg # Ansible config → uses toallab-inventory +├── playbooks/ # Automation workflows +│ ├── provision-vm.yml # VM provisioning with autounattend +│ ├── create-autounattend-iso.yml # Helper for ISO creation +│ ├── patch-vms.yml # Windows Update automation +│ ├── install-iis.yml # IIS deployment demo +│ ├── sync-cmdb.yml # ServiceNow integration +│ └── README-provision.md # Detailed provisioning guide +├── templates/ # Jinja2 templates +│ └── autounattend.xml.j2 # Windows unattended install +├── roles/ # Custom roles (future) +└── collections/ + └── requirements.yml # Required collections ``` +**Inventory Location**: `/home/ptoal/Dev/inventories/toallab-inventory` +- `group_vars/hyperv/` - Hyper-V host configuration +- `group_vars/windows_servers/` - Windows Server defaults +- `host_vars/hyperv1.lan.toal.ca/` - Hypervisor settings + ## Prerequisites - Ansible Automation Platform 2.x @@ -53,6 +90,58 @@ ansible-playbook playbooks/install-iis.yml --limit web_servers - ServiceNow instance (for CMDB integration) - Active Directory domain (for authentication) +## Key Features + +### VM Provisioning +- **Automated Installation**: Uses autounattend.xml for unattended Windows setup +- **Flexible Configuration**: CPU, memory, disk size configurable via variables +- **Network Setup**: Static IP or DHCP configuration +- **WinRM Ready**: Automatically configured for Ansible management + +### Idempotent Operations +- All playbooks can be run multiple times safely +- Check mode support for validation +- Proper state management + +### AAP Integration +- Job template surveys for user-friendly VM creation +- Webhook support for GitOps workflows +- Credential management for secure operations + +## Hypervisor + +**Host**: hyperv1.lan.toal.ca (192.168.1.182) +**Connection**: WinRM over HTTP (NTLM auth) +**Default Storage**: D:\VMs +**Default ISO Path**: D:\ISOs + ## Documentation -See [CLAUDE.md](CLAUDE.md) for detailed architectural documentation and development guidance. +- [CLAUDE.md](CLAUDE.md) - Architecture and development guidelines +- [playbooks/README-provision.md](playbooks/README-provision.md) - VM provisioning guide +- [templates/autounattend.xml.j2](templates/autounattend.xml.j2) - Windows unattended install template + +## Common Tasks + +### Provision a VM +```bash +ansible-playbook playbooks/provision-vm.yml -e vm_name=WEB01 -e vm_ip_address=192.168.1.101 +``` + +### Add VM to Inventory +```bash +# Edit /home/ptoal/Dev/inventories/toallab-inventory/static.yml +# Add under web_servers/app_servers/db_servers: + WEB01: + ansible_host: 192.168.1.101 +``` + +### Configure VM +```bash +ansible-playbook playbooks/install-iis.yml --limit WEB01 +``` + +### Patch VMs +```bash +ansible-playbook playbooks/patch-vms.yml --limit windows_servers +``` diff --git a/ansible.cfg b/ansible.cfg index bc900ca..404c949 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -7,18 +7,5 @@ collections_path = collections interpreter_python = auto_silent timeout = 30 -# Windows-specific settings -ansible_connection = winrm -ansible_winrm_transport = kerberos -ansible_winrm_server_cert_validation = ignore - -# Logging -log_path = ./ansible.log - -[privilege_escalation] -become = True -become_method = runas -become_user = Administrator - [ssh_connection] pipelining = True diff --git a/playbooks/README-provision.md b/playbooks/README-provision.md new file mode 100644 index 0000000..d17e395 --- /dev/null +++ b/playbooks/README-provision.md @@ -0,0 +1,345 @@ +# VM Provisioning Workflow + +This directory contains playbooks for provisioning Windows Server VMs on Hyper-V with automated installation. + +## Quick Start + +### 1. Provision a new VM + +```bash +# Activate virtual environment +source ~/.venv/ansible/bin/activate + +# Run with ansible-navigator (recommended for development) +ansible-navigator run playbooks/provision-vm.yml \ + --execution-environment-image aap.toal.ca/ee-demo \ + --extra-vars "vm_name=WEB01 vm_ip_address=192.168.1.101" + +# Or run directly with ansible-playbook +ansible-playbook playbooks/provision-vm.yml \ + -e vm_name=WEB01 \ + -e vm_ip_address=192.168.1.101 \ + -e vm_cpu_count=4 \ + -e vm_memory_gb=8 +``` + +### 2. Monitor Installation + +The playbook will: +1. ✓ Create the VM with specified resources +2. ✓ Generate autounattend.xml for unattended installation +3. ✓ Attach Windows Server ISO +4. ✓ Start the VM +5. ⏸ Pause for manual verification of installation + +**Manual Steps:** +- Watch VM through Hyper-V Manager or connect via console +- Verify Windows installation progresses automatically +- Wait for system to reach login screen (15-30 minutes) +- Verify WinRM is configured (check if port 5985 is listening) + +### 3. Verify VM + +```bash +# Test WinRM connectivity +ansible WEB01 -i "192.168.1.101," -m ansible.windows.win_ping + +# Or run verification tag +ansible-playbook playbooks/provision-vm.yml -e vm_name=WEB01 --tags verify +``` + +### 4. Add to Inventory + +```bash +# Edit the inventory file +vi /home/ptoal/Dev/inventories/toallab-inventory/static.yml + +# Add under appropriate group (web_servers, app_servers, db_servers): + web_servers: + hosts: + WEB01: + ansible_host: 192.168.1.101 +``` + +### 5. Run Baseline Configuration + +```bash +# Apply Windows baseline configuration (future) +ansible-playbook playbooks/windows-baseline.yml --limit WEB01 + +# Deploy applications +ansible-playbook playbooks/install-iis.yml --limit WEB01 +``` + +## Variables + +### Required Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `vm_name` | Name of the VM to create | `WEB01` | + +### Optional Variables + +| Variable | Description | Default | Example | +|----------|-------------|---------|---------| +| `vm_ip_address` | Static IP address | DHCP | `192.168.1.101` | +| `vm_cpu_count` | Number of CPU cores | `2` | `4` | +| `vm_memory_gb` | Memory in GB | `4` | `8` | +| `vm_disk_size_gb` | Disk size in GB | `60` | `100` | +| `vm_admin_password` | Initial admin password | `P@ssw0rd123!` | `SecureP@ss!` | +| `vm_gateway` | Default gateway | `192.168.1.1` | `192.168.1.1` | +| `vm_subnet_prefix` | Subnet prefix length | `24` | `24` | +| `dns_servers` | DNS server list | From group_vars | `['8.8.8.8']` | + +### Variables from Group Vars + +The following are automatically loaded from group_vars: + +- `default_vm_cpu_count`, `default_vm_memory_gb`, `default_vm_disk_size_gb` +- `default_vm_switch` - Hyper-V virtual switch name +- `vm_storage_path` - Base path for VM files +- `iso_storage_path` - Path to ISO files +- `windows_server_iso` - Path to Windows Server ISO +- `dns_servers` - Default DNS servers +- `timezone` - Windows timezone setting + +## Playbook Tags + +Run specific parts of the playbook using tags: + +```bash +# Only create VM configuration (skip installation) +ansible-playbook provision-vm.yml -e vm_name=WEB01 --tags create + +# Only install/start VM +ansible-playbook provision-vm.yml -e vm_name=WEB01 --tags install + +# Only verify VM status +ansible-playbook provision-vm.yml -e vm_name=WEB01 --tags verify +``` + +## AutoUnattend.xml + +The playbook generates an `autounattend.xml` file that automates Windows installation with: + +- **Partitioning**: Automatic disk partitioning (500MB system, rest for Windows) +- **Locale**: en-US language and keyboard +- **Computer Name**: Set to `vm_name` +- **Timezone**: From `timezone` variable +- **Administrator Password**: From `vm_admin_password` +- **Network**: Static IP if `vm_ip_address` is provided +- **RDP**: Enabled with firewall rules +- **WinRM**: Enabled with firewall rules +- **Firewall**: Initially disabled for setup + +### Using AutoUnattend.xml + +Windows Setup will automatically use `autounattend.xml` if it's found in one of these locations: + +1. **Root of installation media** (requires rebuilding ISO) +2. **Root of removable media** (floppy, USB, second CD drive) +3. **Mounted as additional DVD drive** (easiest for Hyper-V) + +#### Method 1: Second DVD Drive (Recommended) + +```bash +# Run helper playbook to create and mount autounattend ISO +ansible-playbook playbooks/create-autounattend-iso.yml -e vm_name=WEB01 +``` + +#### Method 2: Manual ISO Creation + +```powershell +# On Hyper-V host +# 1. Create ISO with autounattend.xml in root +# Use oscdimg (from Windows ADK) or ImgBurn + +# 2. Mount as second DVD drive +Add-VMDvdDrive -VMName "WEB01" -Path "D:\VMs\WEB01\autounattend.iso" +``` + +#### Method 3: Custom Windows ISO + +Rebuild Windows ISO with autounattend.xml embedded: +- Extract Windows ISO +- Copy autounattend.xml to root +- Rebuild ISO with oscdimg +- Use custom ISO for installation + +## Ansible Automation Platform Integration + +### Job Template Configuration + +**Name:** Provision Windows VM + +**Playbook:** `playbooks/provision-vm.yml` + +**Inventory:** ToalLab + +**Credentials:** +- Machine Credential: `Hyper-V WinRM` + +**Limit:** `hyperv` + +**Survey:** + +| Prompt | Variable | Type | Default | Required | +|--------|----------|------|---------|----------| +| VM Name | `vm_name` | Text | - | Yes | +| IP Address | `vm_ip_address` | Text | - | No | +| CPU Count | `vm_cpu_count` | Integer | 2 | No | +| Memory (GB) | `vm_memory_gb` | Integer | 4 | No | +| Disk Size (GB) | `vm_disk_size_gb` | Integer | 60 | No | + +**Webhook:** Enable for GitOps workflow + +### Workflow Template (Future) + +**Name:** Full VM Lifecycle + +**Nodes:** +1. Provision VM → `provision-vm.yml` +2. Wait for Installation → Manual approval +3. Windows Baseline → `windows-baseline.yml` +4. Deploy Application → `install-iis.yml` +5. Update CMDB → `sync-cmdb.yml` + +## Troubleshooting + +### VM Creation Failed + +```bash +# Check Hyper-V host connectivity +ansible hyperv -m ansible.windows.win_ping + +# Verify paths exist +ansible hyperv -m ansible.windows.win_file -a "path=D:\\VMs state=directory" + +# Check ISO exists +ansible hyperv -m ansible.windows.win_stat -a "path=D:\\ISOs\\Windows_Server_2022.iso" +``` + +### AutoUnattend.xml Not Working + +**Symptoms:** Windows installation shows interactive prompts + +**Causes:** +- AutoUnattend.xml not found by Windows Setup +- XML syntax error +- Missing required sections + +**Solutions:** +1. Verify autounattend.xml is in correct location +2. Check XML is valid (use XMLLint or online validator) +3. Review Windows Setup logs in VM: `C:\Windows\Panther\setupact.log` + +### WinRM Not Available After Installation + +**Symptoms:** Cannot connect via win_ping + +**Causes:** +- First logon commands didn't run +- Firewall blocking WinRM +- Wrong credentials + +**Solutions:** + +```bash +# Connect to VM console through Hyper-V Manager +# Check if first logon commands ran: +Get-Service WinRM +Test-WSMan + +# Manually enable WinRM if needed: +Enable-PSRemoting -Force +Set-Item WSMan:\localhost\Service\Auth\Basic -Value $true + +# Check firewall: +Get-NetFirewallRule -Name "WINRM-HTTP-In-TCP" +``` + +### Installation Hangs + +**Symptoms:** VM stuck at Windows Setup screen + +**Causes:** +- ISO not bootable +- Insufficient resources +- Hardware compatibility issues + +**Solutions:** +1. Increase VM memory (minimum 2GB for Windows Server) +2. Verify ISO integrity +3. Check Hyper-V event logs +4. Try Generation 1 VM instead of Generation 2 + +## Advanced Usage + +### Provision Multiple VMs + +```bash +# Create a loop or use separate job templates in AAP +for vm in WEB01 WEB02 WEB03; do + ansible-playbook provision-vm.yml -e vm_name=$vm -e vm_ip_address=192.168.1.$((100 + ${vm: -1})) +done +``` + +### Custom Windows Edition + +Edit the ISO or autounattend.xml to specify edition: + +```xml + + + + + /IMAGE/NAME + Windows Server 2022 SERVERDATACENTER + + + + +``` + +### Domain Join During Installation + +Add to autounattend.xml under ``: + +```xml + + example.com + DomainJoinPassword + DomainJoinUser + +example.com +``` + +## Files Generated + +After running provision-vm.yml, these files are created on the Hyper-V host: + +``` +D:\VMs\\ +├── .vhdx # Virtual hard disk +├── autounattend.xml # Unattended installation answer file +└── Virtual Machines\ # VM configuration files (created by Hyper-V) +``` + +## Next Steps + +After successful provisioning: + +1. ✓ Add VM to inventory +2. Run baseline configuration (security, monitoring, agents) +3. Join to domain (if required) +4. Deploy applications +5. Update CMDB +6. Configure backup +7. Document in ServiceNow + +## References + +- [Windows Unattended Installation Guide](https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/automate-windows-setup) +- [Hyper-V PowerShell Reference](https://docs.microsoft.com/en-us/powershell/module/hyper-v/) +- [Ansible Windows Guide](https://docs.ansible.com/ansible/latest/os_guide/windows_usage.html) diff --git a/playbooks/create-autounattend-iso.yml b/playbooks/create-autounattend-iso.yml new file mode 100644 index 0000000..d9fb00a --- /dev/null +++ b/playbooks/create-autounattend-iso.yml @@ -0,0 +1,93 @@ +--- +# Helper playbook to create an ISO containing autounattend.xml +# This ISO can be mounted to the VM during installation +# +# Variables: +# vm_name: Name of the VM (required, must match provision-vm.yml) +# +# Usage: +# ansible-playbook create-autounattend-iso.yml -e vm_name=WEB01 + +- name: Create AutoUnattend ISO + hosts: hyperv + gather_facts: false + + vars: + vm_path: "{{ vm_storage_path }}\\{{ vm_name }}" + autounattend_path: "{{ vm_storage_path }}\\{{ vm_name }}\\autounattend.xml" + autounattend_iso_path: "{{ vm_storage_path }}\\{{ vm_name }}\\autounattend.iso" + + tasks: + - name: Validate required variables + ansible.builtin.assert: + that: + - vm_name is defined + - vm_name | length > 0 + fail_msg: "vm_name is required" + + - name: Check if autounattend.xml exists + ansible.windows.win_stat: + path: "{{ autounattend_path }}" + register: autounattend_file + + - name: Verify autounattend.xml exists + ansible.builtin.assert: + that: + - autounattend_file.stat.exists + fail_msg: "AutoUnattend.xml not found at {{ autounattend_path }}. Run provision-vm.yml first." + + - name: Create ISO from autounattend.xml using oscdimg + ansible.windows.win_shell: | + # Use oscdimg from Windows ADK (if installed) + # Otherwise, use PowerShell alternative + + $adkPath = "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg" + $oscdimg = "$adkPath\oscdimg.exe" + + if (Test-Path $oscdimg) { + # Create ISO using oscdimg + & $oscdimg -m -n -b"$adkPath\efisys.bin" ` + "{{ vm_path }}" "{{ autounattend_iso_path }}" + } else { + # Alternative: Use PowerShell to create ISO (requires ImDisk or similar) + # For now, provide instructions + Write-Host "oscdimg not found. Windows ADK required for automated ISO creation." + Write-Host "" + Write-Host "Alternative methods:" + Write-Host "1. Install Windows ADK: https://docs.microsoft.com/en-us/windows-hardware/get-started/adk-install" + Write-Host "2. Use ImgBurn or similar tool manually" + Write-Host "3. Mount autounattend.xml as secondary CD drive during install" + Write-Host "" + Write-Host "To mount as secondary drive:" + Write-Host 'Add-VMDvdDrive -VMName "{{ vm_name }}" -Path "path\to\autounattend.iso"' + exit 1 + } + register: iso_create + failed_when: iso_create.rc != 0 and iso_create.rc != 1 + + - name: Check if ISO was created + ansible.windows.win_stat: + path: "{{ autounattend_iso_path }}" + register: iso_file + when: iso_create.rc == 0 + + - name: Mount AutoUnattend ISO to VM + ansible.windows.win_shell: | + # Add a second DVD drive with the autounattend ISO + Add-VMDvdDrive -VMName "{{ vm_name }}" -Path "{{ autounattend_iso_path }}" + when: + - iso_file is defined + - iso_file.stat.exists | default(false) + + - name: Display results + ansible.builtin.debug: + msg: + - "AutoUnattend ISO creation results:" + - "Source: {{ autounattend_path }}" + - "ISO: {{ autounattend_iso_path }}" + - "Status: {{ 'Created and mounted' if (iso_file.stat.exists | default(false)) else 'Manual intervention required' }}" + - "" + - "If automatic creation failed:" + - "1. Copy {{ autounattend_path }} to the root of a blank CD/ISO" + - "2. Mount the ISO as a second DVD drive to the VM" + - "3. Windows Setup will automatically detect and use it" diff --git a/playbooks/provision-vm.yml b/playbooks/provision-vm.yml index 46eed41..a399684 100644 --- a/playbooks/provision-vm.yml +++ b/playbooks/provision-vm.yml @@ -1,25 +1,274 @@ --- +# 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_hosts + hosts: hyperv gather_facts: false - vars_prompt: - - name: vm_name - prompt: "Enter VM name" - private: false + vars: + # Use defaults from group_vars if not specified + 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: vm_ip_address - prompt: "Enter IP address for VM" - private: false + # 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: 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: Placeholder - Create VM - ansible.builtin.debug: - msg: "Will create VM {{ vm_name }} with IP {{ vm_ip_address }}" + - 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] - # TODO: Implement VM creation using hyper-v modules - # TODO: Generate autounattend.xml from template - # TODO: Attach autounattend.xml to VM - # TODO: Start VM and wait for provisioning - # TODO: Add VM to inventory - # TODO: Update ServiceNow CMDB + - 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 diff --git a/templates/autounattend.xml.j2 b/templates/autounattend.xml.j2 new file mode 100644 index 0000000..719d304 --- /dev/null +++ b/templates/autounattend.xml.j2 @@ -0,0 +1,158 @@ + + + + + + en-US + + en-US + en-US + en-US + en-US + + + + + + + 1 + 500 + Primary + + + 2 + true + Primary + + + + + true + NTFS + + 1 + 1 + + + NTFS + + C + 2 + 2 + + + 0 + true + + + + + + 0 + 2 + + false + + + + true + Ansible Admin + {{ organization | default('ToalLab') }} + + + + + + {{ vm_name }} + {{ timezone | default('Eastern Standard Time') }} + + + false + + + 0 + + + + + true + Remote Desktop + all + + + true + Windows Remote Management + all + + + + + + + + + {{ vm_admin_password | default('P@ssw0rd123!') }} + true</PlainText> + </Password> + <Enabled>true</Enabled> + <LogonCount>3</LogonCount> + <Username>Administrator</Username> + </AutoLogon> + <FirstLogonCommands> + <SynchronousCommand wcm:action="add"> + <Order>1</Order> + <CommandLine>cmd.exe /c netsh advfirewall firewall add rule name="WinRM HTTP" dir=in action=allow protocol=TCP localport=5985</CommandLine> + <Description>Enable WinRM HTTP</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>2</Order> + <CommandLine>cmd.exe /c netsh advfirewall firewall add rule name="WinRM HTTPS" dir=in action=allow protocol=TCP localport=5986</CommandLine> + <Description>Enable WinRM HTTPS</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>3</Order> + <CommandLine>powershell.exe -Command "Enable-PSRemoting -Force"</CommandLine> + <Description>Enable PowerShell Remoting</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>4</Order> + <CommandLine>powershell.exe -Command "Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $true"</CommandLine> + <Description>Enable Basic Auth for WinRM</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>5</Order> + <CommandLine>powershell.exe -Command "Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled False"</CommandLine> + <Description>Disable Windows Firewall for initial setup</Description> + </SynchronousCommand> +{% if vm_ip_address is defined %} + <SynchronousCommand wcm:action="add"> + <Order>6</Order> + <CommandLine>powershell.exe -Command "New-NetIPAddress -InterfaceAlias 'Ethernet' -IPAddress {{ vm_ip_address }} -PrefixLength {{ vm_subnet_prefix | default('24') }} -DefaultGateway {{ vm_gateway | default('192.168.1.1') }}"</CommandLine> + <Description>Set static IP address</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>7</Order> + <CommandLine>powershell.exe -Command "Set-DnsClientServerAddress -InterfaceAlias 'Ethernet' -ServerAddresses {{ dns_servers | join(',') }}"</CommandLine> + <Description>Set DNS servers</Description> + </SynchronousCommand> +{% endif %} + </FirstLogonCommands> + <OOBE> + <HideEULAPage>true</HideEULAPage> + <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <NetworkLocation>Work</NetworkLocation> + <ProtectYourPC>3</ProtectYourPC> + <SkipMachineOOBE>true</SkipMachineOOBE> + <SkipUserOOBE>true</SkipUserOOBE> + </OOBE> + <UserAccounts> + <AdministratorPassword> + <Value>{{ vm_admin_password | default('P@ssw0rd123!') }}</Value> + <PlainText>true</PlainText> + </AdministratorPassword> + </UserAccounts> + </component> + </settings> +</unattend>