# Copyright (c) Microsoft Corporation. # SPDX-License-Identifier: MIT # # <# .SYNOPSIS Creates a Windows virtual machine scale set, set up for vcpkg's CI. .DESCRIPTION create-vmss.ps1 creates an Azure Windows VM scale set, set up for vcpkg's CI system. See https://docs.microsoft.com/en-us/azure/virtual-machine-scale-sets/overview for more information. This script assumes you have installed Azure tools into PowerShell by following the instructions at https://docs.microsoft.com/en-us/powershell/azure/install-az-ps?view=azps-3.6.1 or are running from Azure Cloud Shell. #> $Location = 'SouthCentralUS' $Prefix = 'PrWin-' + (Get-Date -Format 'yyyy-MM-dd') $VMSize = 'Standard_F16s_v2' $ProtoVMName = 'PROTOTYPE' $LiveVMPrefix = 'BUILD' $WindowsServerSku = '2019-Datacenter' $InstalledDiskSizeInGB = 1024 $ErrorActionPreference = 'Stop' $ProgressActivity = 'Creating Scale Set' $TotalProgress = 12 $CurrentProgress = 1 <# .SYNOPSIS Returns whether there's a name collision in the resource group. .DESCRIPTION Find-ResourceGroupNameCollision takes a list of resources, and checks if $Test collides names with any of the resources. .PARAMETER Test The name to test. .PARAMETER Resources The list of resources. #> function Find-ResourceGroupNameCollision { [CmdletBinding()] Param([string]$Test, $Resources) foreach ($resource in $Resources) { if ($resource.ResourceGroupName -eq $Test) { return $true } } return $false } <# .SYNOPSIS Attempts to find a name that does not collide with any resources in the resource group. .DESCRIPTION Find-ResourceGroupName takes a set of resources from Get-AzResourceGroup, and finds the first name in {$Prefix, $Prefix-1, $Prefix-2, ...} such that the name doesn't collide with any of the resources in the resource group. .PARAMETER Prefix The prefix of the final name; the returned name will be of the form "$Prefix(-[1-9][0-9]*)?" #> function Find-ResourceGroupName { [CmdletBinding()] Param([string] $Prefix) $resources = Get-AzResourceGroup $result = $Prefix $suffix = 0 while (Find-ResourceGroupNameCollision -Test $result -Resources $resources) { $suffix++ $result = "$Prefix-$suffix" } return $result } <# .SYNOPSIS Creates a randomly generated password. .DESCRIPTION New-Password generates a password, randomly, of length $Length, containing only alphanumeric characters (both uppercase and lowercase). .PARAMETER Length The length of the returned password. #> function New-Password { Param ([int] $Length = 32) $Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" $result = '' for ($idx = 0; $idx -lt $Length; $idx++) { # NOTE: this should probably use RNGCryptoServiceProvider $result += $Chars[(Get-Random -Minimum 0 -Maximum $Chars.Length)] } return $result } <# .SYNOPSIS Waits for the shutdown of the specified resource. .DESCRIPTION Wait-Shutdown takes a VM, and checks if there's a 'PowerState/stopped' code; if there is, it returns. If there isn't, it waits ten seconds and tries again. .PARAMETER ResourceGroupName The name of the resource group to look up the VM in. .PARAMETER Name The name of the virtual machine to wait on. #> function Wait-Shutdown { [CmdletBinding()] Param([string]$ResourceGroupName, [string]$Name) Write-Host "Waiting for $Name to stop..." while ($true) { $Vm = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $Name -Status $highestStatus = $Vm.Statuses.Count for ($idx = 0; $idx -lt $highestStatus; $idx++) { if ($Vm.Statuses[$idx].Code -eq 'PowerState/stopped') { return } } Write-Host "... not stopped yet, sleeping for 10 seconds" Start-Sleep -Seconds 10 } } <# .SYNOPSIS Sanitizes a name to be used in a storage account. .DESCRIPTION Sanitize-Name takes a string, and removes all of the '-'s and lowercases the string, since storage account names must have no '-'s and must be completely lowercase alphanumeric. It then makes certain that the length of the string is not greater than 24, since that is invalid. .PARAMETER RawName The name to sanitize. #> function Sanitize-Name { [CmdletBinding()] Param( [string]$RawName ) $result = $RawName.Replace('-', '').ToLowerInvariant() if ($result.Length -gt 24) { Write-Error 'Sanitized name for storage account $result was too long.' } return $result } #################################################################################################### Write-Progress ` -Activity $ProgressActivity ` -Status 'Creating resource group' ` -PercentComplete (100 / $TotalProgress * $CurrentProgress++) $ResourceGroupName = Find-ResourceGroupName $Prefix $AdminPW = New-Password New-AzResourceGroup -Name $ResourceGroupName -Location $Location $AdminPWSecure = ConvertTo-SecureString $AdminPW -AsPlainText -Force $Credential = New-Object System.Management.Automation.PSCredential ("AdminUser", $AdminPWSecure) #################################################################################################### Write-Progress ` -Activity $ProgressActivity ` -Status 'Creating virtual network' ` -PercentComplete (100 / $TotalProgress * $CurrentProgress++) $allowHttp = New-AzNetworkSecurityRuleConfig ` -Name AllowHTTP ` -Description 'Allow HTTP(S)' ` -Access Allow ` -Protocol Tcp ` -Direction Outbound ` -Priority 1008 ` -SourceAddressPrefix * ` -SourcePortRange * ` -DestinationAddressPrefix * ` -DestinationPortRange @(80, 443) $allowDns = New-AzNetworkSecurityRuleConfig ` -Name AllowDNS ` -Description 'Allow DNS' ` -Access Allow ` -Protocol * ` -Direction Outbound ` -Priority 1009 ` -SourceAddressPrefix * ` -SourcePortRange * ` -DestinationAddressPrefix * ` -DestinationPortRange 53 $allowStorage = New-AzNetworkSecurityRuleConfig ` -Name AllowStorage ` -Description 'Allow Storage' ` -Access Allow ` -Protocol * ` -Direction Outbound ` -Priority 1010 ` -SourceAddressPrefix VirtualNetwork ` -SourcePortRange * ` -DestinationAddressPrefix Storage ` -DestinationPortRange * $denyEverythingElse = New-AzNetworkSecurityRuleConfig ` -Name DenyElse ` -Description 'Deny everything else' ` -Access Deny ` -Protocol * ` -Direction Outbound ` -Priority 1011 ` -SourceAddressPrefix * ` -SourcePortRange * ` -DestinationAddressPrefix * ` -DestinationPortRange * $NetworkSecurityGroupName = $ResourceGroupName + 'NetworkSecurity' $NetworkSecurityGroup = New-AzNetworkSecurityGroup ` -Name $NetworkSecurityGroupName ` -ResourceGroupName $ResourceGroupName ` -Location $Location ` -SecurityRules @($allowHttp, $allowDns, $allowStorage, $denyEverythingElse) $SubnetName = $ResourceGroupName + 'Subnet' $Subnet = New-AzVirtualNetworkSubnetConfig ` -Name $SubnetName ` -AddressPrefix "10.0.0.0/16" ` -NetworkSecurityGroup $NetworkSecurityGroup $VirtualNetworkName = $ResourceGroupName + 'Network' $VirtualNetwork = New-AzVirtualNetwork ` -Name $VirtualNetworkName ` -ResourceGroupName $ResourceGroupName ` -Location $Location ` -AddressPrefix "10.0.0.0/16" ` -Subnet $Subnet #################################################################################################### Write-Progress ` -Activity $ProgressActivity ` -Status 'Creating archives storage account' ` -PercentComplete (100 / $TotalProgress * $CurrentProgress++) $StorageAccountName = Sanitize-Name $ResourceGroupName New-AzStorageAccount ` -ResourceGroupName $ResourceGroupName ` -Location $Location ` -Name $StorageAccountName ` -SkuName 'Standard_LRS' ` -Kind StorageV2 $StorageAccountKeys = Get-AzStorageAccountKey ` -ResourceGroupName $ResourceGroupName ` -Name $StorageAccountName $StorageAccountKey = $StorageAccountKeys[0].Value $StorageContext = New-AzStorageContext ` -StorageAccountName $StorageAccountName ` -StorageAccountKey $StorageAccountKey $ArchivesFiles = New-AzStorageShare -Name 'archives' -Context $StorageContext Set-AzStorageShareQuota -ShareName 'archives' -Context $StorageContext -Quota 5120 $LogFiles = New-AzStorageShare -Name 'logs' -Context $StorageContext Set-AzStorageShareQuota -ShareName 'logs' -Context $StorageContext -Quota 64 #################################################################################################### Write-Progress ` -Activity 'Creating prototype VM' ` -PercentComplete (100 / $TotalProgress * $CurrentProgress++) $NicName = $ResourceGroupName + 'NIC' $Nic = New-AzNetworkInterface ` -Name $NicName ` -ResourceGroupName $ResourceGroupName ` -Location $Location ` -Subnet $VirtualNetwork.Subnets[0] $VM = New-AzVMConfig -Name $ProtoVMName -VMSize $VMSize $VM = Set-AzVMOperatingSystem ` -VM $VM ` -Windows ` -ComputerName $ProtoVMName ` -Credential $Credential ` -ProvisionVMAgent ` -EnableAutoUpdate $VM = Add-AzVMNetworkInterface -VM $VM -Id $Nic.Id $VM = Set-AzVMSourceImage ` -VM $VM ` -PublisherName 'MicrosoftWindowsServer' ` -Offer 'WindowsServer' ` -Skus $WindowsServerSku ` -Version latest $InstallDiskName = $ProtoVMName + "InstallDisk" $VM = Add-AzVMDataDisk ` -Vm $VM ` -Name $InstallDiskName ` -Lun 0 ` -Caching ReadWrite ` -CreateOption Empty ` -DiskSizeInGB $InstalledDiskSizeInGB ` -StorageAccountType 'StandardSSD_LRS' $VM = Set-AzVMBootDiagnostic -VM $VM -Disable New-AzVm ` -ResourceGroupName $ResourceGroupName ` -Location $Location ` -VM $VM #################################################################################################### Write-Progress ` -Activity $ProgressActivity ` -Status 'Running provisioning script provision-image.ps1 in VM' ` -PercentComplete (100 / $TotalProgress * $CurrentProgress++) Invoke-AzVMRunCommand ` -ResourceGroupName $ResourceGroupName ` -VMName $ProtoVMName ` -CommandId 'RunPowerShellScript' ` -ScriptPath "$PSScriptRoot\provision-image.ps1" ` -Parameter @{AdminUserPassword = $AdminPW; ` StorageAccountName=$StorageAccountName; ` StorageAccountKey=$StorageAccountKey;} #################################################################################################### Write-Progress ` -Activity $ProgressActivity ` -Status 'Restarting VM' ` -PercentComplete (100 / $TotalProgress * $CurrentProgress++) Restart-AzVM -ResourceGroupName $ResourceGroupName -Name $ProtoVMName #################################################################################################### Write-Progress ` -Activity $ProgressActivity ` -Status 'Running provisioning script sysprep.ps1 in VM' ` -PercentComplete (100 / $TotalProgress * $CurrentProgress++) Invoke-AzVMRunCommand ` -ResourceGroupName $ResourceGroupName ` -VMName $ProtoVMName ` -CommandId 'RunPowerShellScript' ` -ScriptPath "$PSScriptRoot\sysprep.ps1" #################################################################################################### Write-Progress ` -Activity $ProgressActivity ` -Status 'Waiting for VM to shut down' ` -PercentComplete (100 / $TotalProgress * $CurrentProgress++) Wait-Shutdown -ResourceGroupName $ResourceGroupName -Name $ProtoVMName #################################################################################################### Write-Progress ` -Activity $ProgressActivity ` -Status 'Converting VM to Image' ` -PercentComplete (100 / $TotalProgress * $CurrentProgress++) Stop-AzVM ` -ResourceGroupName $ResourceGroupName ` -Name $ProtoVMName ` -Force Set-AzVM ` -ResourceGroupName $ResourceGroupName ` -Name $ProtoVMName ` -Generalized $VM = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $ProtoVMName $PrototypeOSDiskName = $VM.StorageProfile.OsDisk.Name $ImageConfig = New-AzImageConfig -Location $Location -SourceVirtualMachineId $VM.ID $Image = New-AzImage -Image $ImageConfig -ImageName $ProtoVMName -ResourceGroupName $ResourceGroupName #################################################################################################### Write-Progress ` -Activity $ProgressActivity ` -Status 'Deleting unused VM and disk' ` -PercentComplete (100 / $TotalProgress * $CurrentProgress++) Remove-AzVM -Id $VM.ID -Force Remove-AzDisk -ResourceGroupName $ResourceGroupName -DiskName $PrototypeOSDiskName -Force Remove-AzDisk -ResourceGroupName $ResourceGroupName -DiskName $InstallDiskName -Force #################################################################################################### Write-Progress ` -Activity $ProgressActivity ` -Status 'Creating scale set' ` -PercentComplete (100 / $TotalProgress * $CurrentProgress++) $VmssIpConfigName = $ResourceGroupName + 'VmssIpConfig' $VmssIpConfig = New-AzVmssIpConfig -SubnetId $Nic.IpConfigurations[0].Subnet.Id -Primary -Name $VmssIpConfigName $VmssName = $ResourceGroupName + 'Vmss' $Vmss = New-AzVmssConfig ` -Location $Location ` -SkuCapacity 6 ` -SkuName $VMSize ` -SkuTier 'Standard' ` -Overprovision $false ` -UpgradePolicyMode Manual $Vmss = Add-AzVmssNetworkInterfaceConfiguration ` -VirtualMachineScaleSet $Vmss ` -Primary $true ` -IpConfiguration $VmssIpConfig ` -NetworkSecurityGroupId $NetworkSecurityGroup.Id ` -Name $NicName $Vmss = Set-AzVmssOsProfile ` -VirtualMachineScaleSet $Vmss ` -ComputerNamePrefix $LiveVMPrefix ` -AdminUsername 'AdminUser' ` -AdminPassword $AdminPW ` -WindowsConfigurationProvisionVMAgent $true ` -WindowsConfigurationEnableAutomaticUpdate $true $Vmss = Set-AzVmssStorageProfile ` -VirtualMachineScaleSet $Vmss ` -OsDiskCreateOption 'FromImage' ` -OsDiskCaching ReadWrite ` -ImageReferenceId $Image.Id New-AzVmss ` -ResourceGroupName $ResourceGroupName ` -Name $VmssName ` -VirtualMachineScaleSet $Vmss #################################################################################################### Write-Progress -Activity $ProgressActivity -Completed Write-Host "Location: $Location" Write-Host "Resource group name: $ResourceGroupName" Write-Host "User name: AdminUser" Write-Host "Using generated password: $AdminPW" Write-Host 'Finished!'