diff --git a/common/template/debian13-gui/debian.pkr.hcl b/common/template/debian13-gui/debian.pkr.hcl index ab40c3c..88789de 100644 --- a/common/template/debian13-gui/debian.pkr.hcl +++ b/common/template/debian13-gui/debian.pkr.hcl @@ -216,10 +216,10 @@ source "vsphere-iso" "debian" { # 4. Navigate down to the linux kernel line (3rd line in the entry) and # jump to its end, then append preseed parameters. # 5. Press Ctrl+X to boot. - boot_wait = "10s" + boot_wait = "20s" boot_command = [ - "", - "e", + "", + "e", "", " auto=true priority=critical", " locale=en_US.UTF-8", diff --git a/common/template/debian13/debian.pkr.hcl b/common/template/debian13/debian.pkr.hcl index a0b45dd..4e47b24 100644 --- a/common/template/debian13/debian.pkr.hcl +++ b/common/template/debian13/debian.pkr.hcl @@ -216,10 +216,10 @@ source "vsphere-iso" "debian" { # 4. Navigate down to the linux kernel line (3rd line in the entry) and # jump to its end, then append preseed parameters. # 5. Press Ctrl+X to boot. - boot_wait = "10s" + boot_wait = "20s" boot_command = [ - "", - "e", + "", + "e", "", " auto=true priority=critical", " locale=en_US.UTF-8", diff --git a/es2025/linux/Create-VMs.ps1 b/es2025/linux/Create-VMs.ps1 index cbdf28e..298588a 100644 --- a/es2025/linux/Create-VMs.ps1 +++ b/es2025/linux/Create-VMs.ps1 @@ -7,14 +7,14 @@ $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: -HQ-SERVER-Linux -HQ-DMZ-Linux -HQ-CLIENT-Linux -INET-HQ-Linux -INET-BRANCH-Linux -HOME-Linux -BR-SERVER-Linux -BR-CLIENT-Linux +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 @@ -33,9 +33,11 @@ param() $ErrorActionPreference = 'Stop' -$TemplateFolderName = 'Templates' -$TargetParentFolderName = 'Euroskills 2025' -$TargetFolderName = 'Linux' +$DatacenterName = 'Ilica' +$TemplateFolderName = 'Templates' +$SkillsFolderName = 'Skills' +$TargetParentFolderName = 'ES2025' +$TargetFolderName = 'Linux' $ServerTemplateName = 'debian-13-template' $GuiTemplateName = 'debian-13-gui-template' $ReferenceSnapshotName = 'start' @@ -51,10 +53,14 @@ $CloudInitShutdownTimeoutSeconds = 900 function Get-SingleFolder { param( [Parameter(Mandatory = $true)] - [string]$Name + [string]$Name, + + $Location = $null ) - $folders = @(Get-Folder -Name $Name -ErrorAction SilentlyContinue) + $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." @@ -76,7 +82,7 @@ function Get-SingleChildFolder { $ParentFolder ) - $folders = @(Get-Folder -Location $ParentFolder -Name $Name -ErrorAction SilentlyContinue) + $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)'." @@ -280,9 +286,11 @@ function Wait-ForVmToPowerOff { throw "VM '$($VM.Name)' did not power off within $TimeoutSeconds seconds." } -$templateFolder = Get-SingleFolder -Name $TemplateFolderName -$targetParentFolder = Get-SingleFolder -Name $TargetParentFolderName -$targetFolder = Get-SingleChildFolder -Name $TargetFolderName -ParentFolder $targetParentFolder +$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 @@ -291,18 +299,18 @@ $serverReferenceSnapshot = Get-OrCreateReferenceSnapshot -VM $serverTemplate -Sn $guiReferenceSnapshot = Get-OrCreateReferenceSnapshot -VM $guiTemplate -SnapshotName $ReferenceSnapshotName $vmDefinitions = @( - @{ Name = 'R-HQ'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('HQ-SERVER-Linux', 'HQ-DMZ-Linux', 'HQ-CLIENT-Linux', 'INET-HQ-Linux') }, - @{ Name = 'R-INT'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('INET-HQ-Linux', 'INET-BRANCH-Linux', 'HOME-Linux') }, - @{ Name = 'R-BR'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('BR-SERVER-Linux', 'BR-CLIENT-Linux', 'INET-BRANCH-Linux') }, - @{ Name = 'HQ-DC'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('HQ-SERVER-Linux') }, - @{ Name = 'HQ-SAM-1'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('HQ-SERVER-Linux') }, - @{ Name = 'HQ-SAM-2'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('HQ-SERVER-Linux') }, - @{ Name = 'HQ-DMZ-1'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('HQ-DMZ-Linux') }, - @{ Name = 'HQ-DMZ-2'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('HQ-DMZ-Linux') }, - @{ Name = 'BR-SRV'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('BR-SERVER-Linux') }, - @{ Name = 'HQ-CL'; Template = $guiTemplate; Snapshot = $guiReferenceSnapshot; Networks = @('HQ-CLIENT-Linux') }, - @{ Name = 'HOME'; Template = $guiTemplate; Snapshot = $guiReferenceSnapshot; Networks = @('HOME-Linux') }, - @{ Name = 'BR-CL'; Template = $guiTemplate; Snapshot = $guiReferenceSnapshot; Networks = @('BR-CLIENT-Linux') } + @{ 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) { @@ -332,6 +340,11 @@ foreach ($definition in $vmDefinitions) { 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 diff --git a/es2025/windows/Create-VMs.ps1 b/es2025/windows/Create-VMs.ps1 new file mode 100644 index 0000000..e7bfa40 --- /dev/null +++ b/es2025/windows/Create-VMs.ps1 @@ -0,0 +1,291 @@ +<# +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" +} diff --git a/ws2024/linux/Create-VMs.ps1 b/ws2024/linux/Create-VMs.ps1 index e15da75..8ff7146 100644 --- a/ws2024/linux/Create-VMs.ps1 +++ b/ws2024/linux/Create-VMs.ps1 @@ -7,10 +7,9 @@ $ServerTemplateName Template VM used for server clones. $GuiTemplateName Template VM used for the workstation clone. This script expects these port groups to already exist: -WAN-Linux -INT-Linux -DMZ-Linux -VPN-Linux +PG-VLAN10 (WAN) +PG-VLAN11 (INT) +PG-VLAN12 (DMZ) What the script does: - Creates the WS2024 Linux topology VMs as linked clones @@ -29,9 +28,11 @@ param() $ErrorActionPreference = 'Stop' -$TemplateFolderName = 'Templates' -$TargetParentFolderName = 'Worldskills 2024' -$TargetFolderName = 'Linux' +$DatacenterName = 'Ilica' +$TemplateFolderName = 'Templates' +$SkillsFolderName = 'Skills' +$TargetParentFolderName = 'WS2024' +$TargetFolderName = 'Linux' $ServerTemplateName = 'debian-13-template' $GuiTemplateName = 'debian-13-gui-template' $ReferenceSnapshotName = 'Start' @@ -47,10 +48,14 @@ $CloudInitShutdownTimeoutSeconds = 900 function Get-SingleFolder { param( [Parameter(Mandatory = $true)] - [string]$Name + [string]$Name, + + $Location = $null ) - $folders = @(Get-Folder -Name $Name -ErrorAction SilentlyContinue) + $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." @@ -72,7 +77,7 @@ function Get-SingleChildFolder { $ParentFolder ) - $folders = @(Get-Folder -Location $ParentFolder -Name $Name -ErrorAction SilentlyContinue) + $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)'." @@ -205,7 +210,9 @@ function New-CloudInitUserData { [string]$Username, [Parameter(Mandatory = $true)] - [string]$Password + [string]$Password, + + [string]$NetworkConfig = '' ) return @" @@ -242,14 +249,49 @@ function Set-CloudInitUserData { [string]$Username, [Parameter(Mandatory = $true)] - [string]$Password + [string]$Password, + + [string]$NetworkConfig = '' ) $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' + + if ($NetworkConfig) { + $metadata = @" +instance-id: $($VM.Name) +local-hostname: $($VM.Name) +$NetworkConfig +"@ + $encodedMetadata = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($metadata)) + Set-AdvancedSettingValue -Entity $VM -Name 'guestinfo.metadata' -Value $encodedMetadata + Set-AdvancedSettingValue -Entity $VM -Name 'guestinfo.metadata.encoding' -Value 'base64' + } +} + +function New-NetConfig { + param([hashtable[]]$Nics) + + $yaml = "network:`n version: 2`n ethernets:" + + foreach ($nic in $Nics) { + $addrs = @($nic.Ipv4, $nic.Ipv6) | Where-Object { $_ } | ForEach-Object { "`"$_`"" } + $yaml += "`n $($nic.Name):" + $yaml += "`n addresses: [$($addrs -join ', ')]" + + $routes = @() + if ($nic.Gw4) { $routes += "- to: default`n via: $($nic.Gw4)" } + if ($nic.Gw6) { $routes += "- to: `"::/0`"`n via: `"$($nic.Gw6)`"" } + if ($routes) { + $yaml += "`n routes:" + $routes | ForEach-Object { $yaml += "`n $_" } + } + + } + + return $yaml } function Wait-ForVmToPowerOff { @@ -276,9 +318,11 @@ function Wait-ForVmToPowerOff { throw "VM '$($VM.Name)' did not power off within $TimeoutSeconds seconds." } -$templateFolder = Get-SingleFolder -Name $TemplateFolderName -$targetParentFolder = Get-SingleFolder -Name $TargetParentFolderName -$targetFolder = Get-SingleChildFolder -Name $TargetFolderName -ParentFolder $targetParentFolder +$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 @@ -286,15 +330,21 @@ $guiTemplate = Get-SingleVmFromFolder -Name $GuiTemplateName -Folder $templateFo $serverReferenceSnapshot = Get-OrCreateReferenceSnapshot -VM $serverTemplate -SnapshotName $ReferenceSnapshotName $guiReferenceSnapshot = Get-OrCreateReferenceSnapshot -VM $guiTemplate -SnapshotName $ReferenceSnapshotName +# ── Network segment variables ───────────────────────────────────────────────── +$WanGw4 = '1.1.1.10'; $WanGw6 = '2001:db8:1111::10' +$IntGw4 = '10.1.10.1'; $IntGw6 = '2001:db8:1001:10::1' +$DmzGw4 = '10.1.20.1'; $DmzGw6 = '2001:db8:1001:20::1' + +# ── VM definitions ──────────────────────────────────────────────────────────── $vmDefinitions = @( - @{ Name = 'fw'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('WAN-Linux', 'INT-Linux', 'DMZ-Linux', 'VPN-Linux') }, - @{ Name = 'int-srv01'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('INT-Linux') }, - @{ Name = 'mail'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('DMZ-Linux') }, - @{ Name = 'ha-prx01'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('DMZ-Linux') }, - @{ Name = 'ha-prx02'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('DMZ-Linux') }, - @{ Name = 'web01'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('DMZ-Linux') }, - @{ Name = 'web02'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('DMZ-Linux') }, - @{ Name = 'jamie-ws01'; Template = $guiTemplate; Snapshot = $guiReferenceSnapshot; Networks = @('WAN-Linux', 'VPN-Linux') } + @{ Name = 'fw'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN10', 'PG-VLAN11', 'PG-VLAN12') }, + @{ Name = 'int-srv01'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN11') }, + @{ Name = 'mail'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN12') }, + @{ Name = 'ha-prx01'; Template = $guiTemplate; Snapshot = $guiReferenceSnapshot; Networks = @('PG-VLAN12') }, + @{ Name = 'ha-prx02'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN12') }, + @{ Name = 'web01'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN12') }, + @{ Name = 'web02'; Template = $serverTemplate; Snapshot = $serverReferenceSnapshot; Networks = @('PG-VLAN12') }, + @{ Name = 'jamie-ws01'; Template = $guiTemplate; Snapshot = $guiReferenceSnapshot; Networks = @('PG-VLAN10') } ) foreach ($definition in $vmDefinitions) { @@ -323,7 +373,12 @@ foreach ($definition in $vmDefinitions) { Ensure-ExtraDisks -VM $vm -DiskCount $ExtraDiskCount -DiskSizeGB $ExtraDiskSizeGB Set-ExactNetworkAdapters -VM $vm -PortGroupNames $definition.Networks - Set-CloudInitUserData -VM $vm -Username $DefaultUsername -Password $DefaultPassword + Set-CloudInitUserData -VM $vm -Username $DefaultUsername -Password $DefaultPassword -NetworkConfig $definition.NetworkConfig + $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 diff --git a/ws2024/windows/Create-VMs.ps1 b/ws2024/windows/Create-VMs.ps1 new file mode 100644 index 0000000..ee948e2 --- /dev/null +++ b/ws2024/windows/Create-VMs.ps1 @@ -0,0 +1,293 @@ +<# +Adjust these values only if your vCenter names are different: +$WinTemplateName Template VM used for all Windows Server VMs. +$LinuxTemplateName Template VM used for ANSIBLE-SRV (Debian 13 GUI). + +This script expects these port groups to already exist: +PG-VLAN10 (Paris-Core) +PG-VLAN11 (Paris-Dist) +PG-VLAN12 (Paris-Client) +PG-VLAN20 (LA-Paris) +PG-VLAN21 (LA-Lyon) +PG-VLAN23 (Lyon-Remote) + +What the script does: +- Creates the WS2024 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 ANSIBLE-SRV only +- Powers on the VM, waits for shutdown, then creates a snapshot named Start + +Run: +.\Create-VMs.ps1 +#> + +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' + +$DatacenterName = 'Ilica' +$SkillsFolderName = 'Skills' +$TargetParentFolderName = 'WS2024' +$TargetFolderName = 'Windows' +$TemplateFolderName = 'Templates' +$WinTemplateName = 'ws2022' +$LinuxTemplateName = 'debian-13-gui-template' +$ReferenceSnapshotName = 'Start' +$StartSnapshotName = 'Start' +$CpuCount = 2 +$MemoryGB = 4 +$DefaultUsername = 'user' +$DefaultPassword = 'Skill39@Lyon' +$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 = 'DC1'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN10'); IsLinux = $false }, + @{ Name = 'NW-SRV'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN10'); IsLinux = $false }, + @{ Name = 'FILE-SRV'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN11'); IsLinux = $false }, + @{ Name = 'WEB-SRV'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN11'); IsLinux = $false }, + @{ Name = 'PARIS-ROUTER'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN10', 'PG-VLAN11', 'PG-VLAN12', 'PG-VLAN20'); IsLinux = $false }, + @{ Name = 'LA-ROUTER'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN20', 'PG-VLAN21'); IsLinux = $false }, + @{ Name = 'LYON-ROUTER'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN21', 'PG-VLAN23'); IsLinux = $false }, + @{ Name = 'DC2'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN23'); IsLinux = $false }, + @{ Name = 'WIN-CLIENT1'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN12'); IsLinux = $false }, + @{ Name = 'WIN-CLIENT2'; Template = $winTemplate; Snapshot = $winSnapshot; Networks = @('PG-VLAN23'); IsLinux = $false }, + @{ Name = 'ANSIBLE-SRV'; Template = $linuxTemplate; Snapshot = $linuxSnapshot; Networks = @('PG-VLAN12'); IsLinux = $true } +) + +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" +}