<# Adjust these values only if your vCenter names are different: $WinTemplateName Template VM used for all Windows Server VMs. $LinuxTemplateName Template VM used for DEV-PC (Debian 13 GUI). This script expects these port groups to already exist: PG-VLAN10 (CPH-INT) PG-VLAN11 (CPH-DEV) PG-VLAN12 (INET) PG-VLAN20 (AAL-INT) What the script does: - Creates the ES2025 Windows topology VMs as linked clones - Sets each VM to 2 vCPU and 4 GB RAM - Configures network adapters to match the topology - Injects cloud-init userdata for DEV-PC only - Windows VMs are snapshotted without powering on - DEV-PC boots, runs cloud-init, powers off, then gets snapshotted Run: .\Create-VMs.ps1 #> [CmdletBinding()] param() $ErrorActionPreference = 'Stop' $DatacenterName = 'Ilica' $SkillsFolderName = 'Skills' $TargetParentFolderName = 'ES2025' $TargetFolderName = 'Windows' $TemplateFolderName = 'Templates' $WinTemplateName = 'ws2022' $LinuxTemplateName = 'debian-13-gui-template' $ReferenceSnapshotName = 'Start' $StartSnapshotName = 'Start' $CpuCount = 2 $MemoryGB = 4 $DefaultUsername = 'user' $DefaultPassword = 'Passw0rd!' $CloudInitShutdownTimeoutSeconds = 900 function Get-SingleFolder { param( [Parameter(Mandatory = $true)] [string]$Name, $Location = $null ) $params = @{ Name = $Name; Type = 'VM'; ErrorAction = 'SilentlyContinue' } if ($Location) { $params['Location'] = $Location } $folders = @(Get-Folder @params) if ($folders.Count -eq 0) { throw "Folder '$Name' was not found." } if ($folders.Count -gt 1) { throw "More than one folder named '$Name' was found." } return $folders[0] } function Get-SingleChildFolder { param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] $ParentFolder ) $folders = @(Get-Folder -Location $ParentFolder -Name $Name -Type VM -ErrorAction SilentlyContinue) if ($folders.Count -eq 0) { throw "Folder '$Name' was not found under '$($ParentFolder.Name)'." } if ($folders.Count -gt 1) { throw "More than one folder named '$Name' was found under '$($ParentFolder.Name)'." } return $folders[0] } function Get-SingleVmFromFolder { param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] $Folder ) $vms = @(Get-VM -Location $Folder -Name $Name -ErrorAction SilentlyContinue) if ($vms.Count -eq 0) { throw "VM '$Name' was not found in folder '$($Folder.Name)'." } if ($vms.Count -gt 1) { throw "More than one VM named '$Name' was found in folder '$($Folder.Name)'." } return $vms[0] } function Get-OrCreateReferenceSnapshot { param( [Parameter(Mandatory = $true)] $VM, [Parameter(Mandatory = $true)] [string]$SnapshotName ) $snapshot = Get-Snapshot -VM $VM -Name $SnapshotName -ErrorAction SilentlyContinue if (-not $snapshot) { Write-Host "Creating snapshot '$SnapshotName' on '$($VM.Name)'" $snapshot = New-Snapshot -VM $VM -Name $SnapshotName -Description 'Base snapshot for linked clones' -Memory:$false -Quiesce:$false } return $snapshot } function Set-ExactNetworkAdapters { param( [Parameter(Mandatory = $true)] $VM, [Parameter(Mandatory = $true)] [string[]]$PortGroupNames ) $adapters = @(Get-NetworkAdapter -VM $VM | Sort-Object Name) while ($adapters.Count -gt $PortGroupNames.Count) { Remove-NetworkAdapter -NetworkAdapter $adapters[-1] -Confirm:$false | Out-Null $adapters = @(Get-NetworkAdapter -VM $VM | Sort-Object Name) } for ($i = 0; $i -lt $PortGroupNames.Count; $i++) { if ($i -ge $adapters.Count) { New-NetworkAdapter -VM $VM -Type Vmxnet3 -NetworkName $PortGroupNames[$i] -StartConnected:$true -Confirm:$false | Out-Null $adapters = @(Get-NetworkAdapter -VM $VM | Sort-Object Name) } Set-NetworkAdapter -NetworkAdapter $adapters[$i] -NetworkName $PortGroupNames[$i] -StartConnected:$true -Confirm:$false | Out-Null } } function Set-AdvancedSettingValue { param( [Parameter(Mandatory = $true)] $Entity, [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [string]$Value ) $setting = Get-AdvancedSetting -Entity $Entity -Name $Name -ErrorAction SilentlyContinue if ($setting) { Set-AdvancedSetting -AdvancedSetting $setting -Value $Value -Confirm:$false | Out-Null return } New-AdvancedSetting -Entity $Entity -Name $Name -Value $Value -Confirm:$false | Out-Null } function Set-CloudInitUserData { param( [Parameter(Mandatory = $true)] $VM, [Parameter(Mandatory = $true)] [string]$Username, [Parameter(Mandatory = $true)] [string]$Password ) $userData = @" #cloud-config hostname: $($VM.Name) users: - default - name: $Username lock_passwd: false plain_text_passwd: '$Password' shell: /bin/bash sudo: ALL=(ALL) NOPASSWD:ALL groups: sudo ssh_pwauth: true disable_root: false chpasswd: expire: false list: | root:$Password ${Username}:$Password power_state: mode: poweroff timeout: 30 condition: true "@ $encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userData)) Set-AdvancedSettingValue -Entity $VM -Name 'guestinfo.userdata' -Value $encoded Set-AdvancedSettingValue -Entity $VM -Name 'guestinfo.userdata.encoding' -Value 'base64' } function Wait-ForVmToPowerOff { param( [Parameter(Mandatory = $true)] $VM, [Parameter(Mandatory = $true)] [int]$TimeoutSeconds ) $deadline = (Get-Date).AddSeconds($TimeoutSeconds) do { if ((Get-VM -Id $VM.Id).PowerState -eq 'PoweredOff') { return } Start-Sleep -Seconds 5 } while ((Get-Date) -lt $deadline) throw "VM '$($VM.Name)' did not power off within $TimeoutSeconds seconds." } # ── Folder and template lookup ──────────────────────────────────────────────── $datacenter = Get-Datacenter -Name $DatacenterName -ErrorAction Stop $skillsFolder = Get-SingleFolder -Name $SkillsFolderName -Location $datacenter $templateFolder = Get-SingleChildFolder -Name $TemplateFolderName -ParentFolder $skillsFolder $targetParentFolder = Get-SingleChildFolder -Name $TargetParentFolderName -ParentFolder $skillsFolder $targetFolder = Get-SingleChildFolder -Name $TargetFolderName -ParentFolder $targetParentFolder $winTemplate = Get-SingleVmFromFolder -Name $WinTemplateName -Folder $templateFolder $linuxTemplate = Get-SingleVmFromFolder -Name $LinuxTemplateName -Folder $templateFolder $winSnapshot = Get-OrCreateReferenceSnapshot -VM $winTemplate -SnapshotName $ReferenceSnapshotName $linuxSnapshot = Get-OrCreateReferenceSnapshot -VM $linuxTemplate -SnapshotName $ReferenceSnapshotName # ── VM definitions ──────────────────────────────────────────────────────────── $vmDefinitions = @( @{ Name = 'INET'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN12'); IsLinux = $false }, @{ Name = 'RTR-CPH'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN12', 'PG-VLAN10', 'PG-VLAN11'); IsLinux = $false }, @{ Name = 'RTR-AAL'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN12', 'PG-VLAN20'); IsLinux = $false }, @{ Name = 'DC'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN10'); IsLinux = $false }, @{ Name = 'SRV1'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN10'); IsLinux = $false }, @{ Name = 'SRV2'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN10'); IsLinux = $false }, @{ Name = 'RODC'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN20'); IsLinux = $false }, @{ Name = 'DEV-SRV'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN11'); IsLinux = $false }, @{ Name = 'DEV-PC'; Template = $linuxTemplate; Snapshot = $linuxSnapshot; Networks = @('PG-VLAN11'); IsLinux = $true }, @{ Name = 'CLIENT'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN20'); IsLinux = $false } ) foreach ($definition in $vmDefinitions) { $vm = Get-VM -Name $definition.Name -ErrorAction SilentlyContinue if (-not $vm) { Write-Host "Creating $($definition.Name)" $resourcePool = Get-ResourcePool -Id $definition.Template.ExtensionData.ResourcePool $vmHost = Get-VMHost -Id $definition.Template.ExtensionData.Runtime.Host $vm = New-VM ` -Name $definition.Name ` -VM $definition.Template ` -ReferenceSnapshot $definition.Snapshot ` -LinkedClone ` -Location $targetFolder ` -ResourcePool $resourcePool ` -VMHost $vmHost } else { Write-Host "$($definition.Name) already exists, updating configuration" } Set-VM -VM $vm -NumCpu $CpuCount -MemoryGB $MemoryGB -Confirm:$false | Out-Null Set-ExactNetworkAdapters -VM $vm -PortGroupNames $definition.Networks $templateCdroms = @(Get-CDDrive -VM $definition.Template | Sort-Object Name) $vmCdroms = @(Get-CDDrive -VM $vm | Sort-Object Name) for ($i = 0; $i -lt $templateCdroms.Count; $i++) { $vmCdroms[$i] | Set-CDDrive -IsoPath $templateCdroms[$i].IsoPath -StartConnected $true -Confirm:$false | Out-Null } if ($definition.IsLinux) { Set-CloudInitUserData -VM $vm -Username $DefaultUsername -Password $DefaultPassword } $startSnapshot = Get-Snapshot -VM $vm -Name $StartSnapshotName -ErrorAction SilentlyContinue if (-not $startSnapshot) { if ($definition.IsLinux) { if ((Get-VM -Id $vm.Id).PowerState -eq 'PoweredOn') { Stop-VMGuest -VM $vm -Confirm:$false -ErrorAction SilentlyContinue | Out-Null Wait-ForVmToPowerOff -VM $vm -TimeoutSeconds $CloudInitShutdownTimeoutSeconds } Start-VM -VM $vm | Out-Null Wait-ForVmToPowerOff -VM $vm -TimeoutSeconds $CloudInitShutdownTimeoutSeconds } New-Snapshot -VM $vm -Name $StartSnapshotName -Description 'Initial state' -Memory:$false -Quiesce:$false | Out-Null } Write-Host "$($definition.Name) ready" }