<# Adjust these values only if your vCenter names are different: $TemplateFolderName Folder that contains the source template VMs. $TargetParentFolderName Parent folder that contains the target folder. $TargetFolderName Folder where the linked clones will be created. $ServerTemplateName Template VM used for server and router clones. $GuiTemplateName Template VM used for workstation clones. This script expects these port groups to already exist: PG-VLAN10 (HQ-SERVER) PG-VLAN11 (HQ-DMZ) PG-VLAN12 (HQ-CLIENT) PG-VLAN20 (INET-HQ) PG-VLAN21 (INET-BRANCH) PG-VLAN23 (HOME) PG-VLAN30 (BR-SERVER) PG-VLAN51 (BR-CLIENT) What the script does: - Creates the ES2025 Linux topology VMs as linked clones - Sets each VM to 2 vCPU and 4 GB RAM - Adds 4 extra 10 GB thin disks - Configures network adapters to match the topology - Injects cloud-init userdata for root and user - Powers on the VM, waits for cloud-init shutdown, then creates a snapshot named start Run: .\Create-VMs.ps1 #> [CmdletBinding()] param() $ErrorActionPreference = 'Stop' $DatacenterName = 'Ilica' $TemplateFolderName = 'Templates' $SkillsFolderName = 'Skills' $TargetParentFolderName = 'ES2025' $TargetFolderName = 'Linux' $ServerTemplateName = 'debian-13-template' $GuiTemplateName = 'debian-13-gui-template' $ReferenceSnapshotName = 'start' $StartSnapshotName = 'start' $CpuCount = 2 $MemoryGB = 4 $ExtraDiskCount = 4 $ExtraDiskSizeGB = 10 $DefaultUsername = 'localadmin' $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. Make the name unique before running this script." } 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 source VM '$($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) { $adapterToRemove = $adapters[-1] Remove-NetworkAdapter -NetworkAdapter $adapterToRemove -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 Ensure-ExtraDisks { param( [Parameter(Mandatory = $true)] $VM, [Parameter(Mandatory = $true)] [int]$DiskCount, [Parameter(Mandatory = $true)] [int]$DiskSizeGB ) $hardDisks = @(Get-HardDisk -VM $VM | Sort-Object Name) $extraDisksNeeded = $DiskCount - ($hardDisks.Count - 1) while ($extraDisksNeeded -gt 0) { New-HardDisk -VM $VM -CapacityGB $DiskSizeGB -StorageFormat Thin | Out-Null $extraDisksNeeded-- } } 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 New-CloudInitUserData { param( [Parameter(Mandatory = $true)] [string]$VmName, [Parameter(Mandatory = $true)] [string]$Username, [Parameter(Mandatory = $true)] [string]$Password ) return @" #cloud-config hostname: $VmName 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 "@ } function Set-CloudInitUserData { param( [Parameter(Mandatory = $true)] $VM, [Parameter(Mandatory = $true)] [string]$Username, [Parameter(Mandatory = $true)] [string]$Password ) $userData = New-CloudInitUserData -VmName $VM.Name -Username $Username -Password $Password $encodedUserData = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userData)) Set-AdvancedSettingValue -Entity $VM -Name 'guestinfo.userdata' -Value $encodedUserData 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 { $currentVm = Get-VM -Id $VM.Id if ($currentVm.PowerState -eq 'PoweredOff') { return } Start-Sleep -Seconds 5 } while ((Get-Date) -lt $deadline) throw "VM '$($VM.Name)' did not power off within $TimeoutSeconds seconds." } $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 $serverTemplate = Get-SingleVmFromFolder -Name $ServerTemplateName -Folder $templateFolder $guiTemplate = Get-SingleVmFromFolder -Name $GuiTemplateName -Folder $templateFolder $serverReferenceSnapshot = Get-OrCreateReferenceSnapshot -VM $serverTemplate -SnapshotName $ReferenceSnapshotName $guiReferenceSnapshot = Get-OrCreateReferenceSnapshot -VM $guiTemplate -SnapshotName $ReferenceSnapshotName $vmDefinitions = @( @{ Name = 'R-HQ'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN10', 'PG-VLAN11', 'PG-VLAN12', 'PG-VLAN20') }, @{ Name = 'R-INT'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN20', 'PG-VLAN21', 'PG-VLAN23') }, @{ Name = 'R-BR'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN30', 'PG-VLAN51', 'PG-VLAN21') }, @{ Name = 'HQ-DC'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN10') }, @{ Name = 'HQ-SAM-1'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN10') }, @{ Name = 'HQ-SAM-2'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN10') }, @{ Name = 'HQ-DMZ-1'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN11') }, @{ Name = 'HQ-DMZ-2'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN11') }, @{ Name = 'BR-SRV'; Template = $guiTemplate; Snapshot = $guiReferenceSnapshot; Networks = @('PG-VLAN30') }, @{ Name = 'HQ-CL'; Template = $guiTemplate; Snapshot = $guiReferenceSnapshot; Networks = @('PG-VLAN12') }, @{ Name = 'HOME'; Template = $guiTemplate; Snapshot = $guiReferenceSnapshot; Networks = @('PG-VLAN23') }, @{ Name = 'BR-CL'; Template = $guiTemplate; Snapshot = $guiReferenceSnapshot; Networks = @('PG-VLAN51') } ) 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 Ensure-ExtraDisks -VM $vm -DiskCount $ExtraDiskCount -DiskSizeGB $ExtraDiskSizeGB Set-ExactNetworkAdapters -VM $vm -PortGroupNames $definition.Networks Set-CloudInitUserData -VM $vm -Username $DefaultUsername -Password $DefaultPassword $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 } $startSnapshot = Get-Snapshot -VM $vm -Name $StartSnapshotName -ErrorAction SilentlyContinue if (-not $startSnapshot) { 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 after cloud-init customization' -Memory:$false -Quiesce:$false | Out-Null } Write-Host "$($definition.Name) ready" }