# Verify Running as Admin $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") If (-not $isAdmin) { Write-Host "-- Restarting as Administrator" -ForegroundColor Cyan ; Start-Sleep -Seconds 1 if($PSVersionTable.PSEdition -eq "Core") { Start-Process pwsh.exe "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs } else { Start-Process powershell.exe "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs } exit } #region Functions #region Output logging function WriteInfo($message) { Write-Host $message } function WriteInfoHighlighted($message) { Write-Host $message -ForegroundColor Cyan } function WriteSuccess($message) { Write-Host $message -ForegroundColor Green } function WriteError($message) { Write-Host $message -ForegroundColor Red } function WriteErrorAndExit($message) { Write-Host $message -ForegroundColor Red Write-Host "Press enter to continue ..." Stop-Transcript Read-Host | Out-Null Exit } #endregion #region Telemetry Function Merge-Hashtables { $Output = @{} ForEach ($Hashtable in ($Input + $Args)) { If ($Hashtable -is [Hashtable]) { ForEach ($Key in $Hashtable.Keys) {$Output.$Key = $Hashtable.$Key} } } $Output } function Get-StringHash { [CmdletBinding()] param ( [Parameter(ValueFromPipeline, Mandatory = $true)] [string]$String, $Hash = "SHA1" ) process { $bytes = [System.Text.Encoding]::UTF8.GetBytes($String) $algorithm = [System.Security.Cryptography.HashAlgorithm]::Create($Hash) $StringBuilder = New-Object System.Text.StringBuilder $algorithm.ComputeHash($bytes) | ForEach-Object { $null = $StringBuilder.Append($_.ToString("x2")) } $StringBuilder.ToString() } } function Get-VolumePhysicalDisk { param( [Parameter(Mandatory = $true)] [string]$Volume ) process { if(-not $Volume.EndsWith(":")) { $Volume += ":" } $physicalDisks = Get-cimInstance "win32_diskdrive" foreach($disk in $physicalDisks) { $partitions = Get-cimInstance -Query "ASSOCIATORS OF {Win32_DiskDrive.DeviceID=`"$($disk.DeviceID.replace('\','\\'))`"} WHERE AssocClass = Win32_DiskDriveToDiskPartition" foreach($partition in $partitions) { $partitionVolumes = Get-cimInstance -Query "ASSOCIATORS OF {Win32_DiskPartition.DeviceID=`"$($partition.DeviceID)`"} WHERE AssocClass = Win32_LogicalDiskToPartition" foreach($partitionVolume in $partitionVolumes) { if($partitionVolume.Name -eq $Volume) { $physicalDisk = Get-PhysicalDisk | Where-Object DeviceID -eq $disk.Index return $physicalDisk } } } } } } function Get-TelemetryLevel { param( [switch]$OptOut ) process { $acceptedTelemetryLevels = "None", "Basic", "Full" # LabConfig value has a priority if($LabConfig.TelemetryLevel -and $LabConfig.TelemetryLevel -in $acceptedTelemetryLevels) { return $LabConfig.TelemetryLevel } # Environment variable as a fallback if($env:WSLAB_TELEMETRY_LEVEL -and $env:WSLAB_TELEMETRY_LEVEL -in $acceptedTelemetryLevels) { return $env:WSLAB_TELEMETRY_LEVEL } # If nothing is explicitely configured and OptOut flag enabled, explicitely disable telemetry if($OptOut) { return "None" } # as a last option return nothing to allow asking the user } } function Get-TelemetryLevelSource { param( [switch]$OptOut ) process { $acceptedTelemetryLevels = "None", "Basic", "Full" # Is it set interactively? if($LabConfig.ContainsKey("TelemetryLevelSource")) { return $LabConfig.TelemetryLevelSource } # LabConfig value has a priority if($LabConfig.TelemetryLevel -and $LabConfig.TelemetryLevel -in $acceptedTelemetryLevels) { return "LabConfig" } # Environment variable as a fallback if($env:WSLAB_TELEMETRY_LEVEL -and $env:WSLAB_TELEMETRY_LEVEL -in $acceptedTelemetryLevels) { return "Environment" } } } function Get-PcSystemType { param( [Parameter(Mandatory = $true)] [int]$Id ) process { $type = switch($Id) { 1 { "Desktop" } 2 { "Laptop" } 3 { "Workstation" } 4 { "Server" } 7 { "Server" } 5 { "Server" } default { $Id } } $type } } $aiPropertyCache = @{} function New-TelemetryEvent { param( [Parameter(Mandatory = $true)] [string]$Event, $Properties, $Metrics, $NickName ) process { if(-not $TelemetryInstrumentationKey) { WriteInfo "Instrumentation key is required to send telemetry data." return } $level = Get-TelemetryLevel $levelSource = Get-TelemetryLevelSource $r = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" $build = "$($r.CurrentMajorVersionNumber).$($r.CurrentMinorVersionNumber).$($r.CurrentBuildNumber).$($r.UBR)" $osVersion = "$($r.ProductName) ($build)" $hw = Get-CimInstance -ClassName Win32_ComputerSystem $os = Get-CimInstance -ClassName Win32_OperatingSystem $machineHash = (((Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Cryptography).MachineGuid) | Get-StringHash) if(-not $NickName) { $NickName = "?" } $osType = switch ($os.ProductType) { 1 { "Workstation" } default { "Server" } } $extraMetrics = @{} $extraProperties = @{ 'telemetry.level' = $level 'telemetry.levelSource' = $levelSource 'telemetry.nick' = $NickName 'powershell.edition' = $PSVersionTable.PSEdition 'powershell.version' = $PSVersionTable.PSVersion.ToString() 'host.isAzure' = (Get-CimInstance win32_systemenclosure).SMBIOSAssetTag -eq "7783-7084-3265-9085-8269-3286-77" 'host.os.type' = $osType 'host.os.build' = $r.CurrentBuildNumber 'hw.type' = Get-PcSystemType -Id $hw.PCSystemType } if($level -eq "Full") { # OS $extraProperties.'device.locale' = (Get-WinsystemLocale).Name # RAM $extraMetrics.'memory.total' = [Math]::Round(($hw.TotalPhysicalMemory)/1024KB, 0) # CPU $extraMetrics.'cpu.logical.count' = $hw.NumberOfLogicalProcessors $extraMetrics.'cpu.sockets.count' = $hw.NumberOfProcessors if(-not $aiPropertyCache.ContainsKey("cpu.model")) { $aiPropertyCache["cpu.model"] = (Get-CimInstance "Win32_Processor" | Select-Object -First 1).Name } $extraProperties.'cpu.model' = $aiPropertyCache["cpu.model"] # Disk $driveLetter = $ScriptRoot -Split ":" | Select-Object -First 1 $volume = Get-Volume -DriveLetter $driveLetter $disk = Get-VolumePhysicalDisk -Volume $driveLetter $extraMetrics.'volume.size' = [Math]::Round($volume.Size / 1024MB) $extraProperties.'volume.fs' = $volume.FileSystemType $extraProperties.'disk.type' = $disk.MediaType $extraProperties.'disk.busType' = $disk.BusType } $payload = @{ name = "Microsoft.ApplicationInsights.Event" time = $([System.dateTime]::UtcNow.ToString("o")) iKey = $TelemetryInstrumentationKey tags = @{ "ai.internal.sdkVersion" = 'wslab-telemetry:1.0.1' "ai.application.ver" = $wslabVersion "ai.cloud.role" = Split-Path -Path $PSCommandPath -Leaf "ai.session.id" = $TelemetrySessionId "ai.user.id" = $machineHash "ai.device.id" = $machineHash "ai.device.type" = $extraProperties["hw.type"] "ai.device.locale" = "" # not propagated anymore "ai.device.os" = "" "ai.device.osVersion" = "" "ai.device.oemName" = "" "ai.device.model" = "" } data = @{ baseType = "EventData" baseData = @{ ver = 2 name = $Event properties = ($Properties, $extraProperties | Merge-Hashtables) measurements = ($Metrics, $extraMetrics | Merge-Hashtables) } } } if($level -eq "Full") { $payload.tags.'ai.device.os' = $osVersion $payload.tags.'ai.device.osVersion' = $build } $payload } } function Send-TelemetryObject { param( [Parameter(Mandatory = $true)] [array]$Data ) process { $json = "{0}" -f (($Data) | ConvertTo-Json -Depth 10 -Compress) if($LabConfig.ContainsKey('TelemetryDebugLog')) { Add-Content -Path "$ScriptRoot\Telemetry.log" -Value ((Get-Date -Format "s") + "`n" + $json) } try { $response = Invoke-RestMethod -Uri 'https://dc.services.visualstudio.com/v2/track' -Method Post -UseBasicParsing -Body $json -TimeoutSec 20 } catch { WriteInfo "`tSending telemetry failed with an error: $($_.Exception.Message)" $response = $_.Exception.Message } if($LabConfig.ContainsKey('TelemetryDebugLog')) { Add-Content -Path "$ScriptRoot\Telemetry.log" -Value $response Add-Content -Path "$ScriptRoot\Telemetry.log" -Value "`n------------------------------`n" } } } function Send-TelemetryEvent { param( [Parameter(Mandatory = $true)] [string]$Event, $Properties, $Metrics, $NickName ) process { $telemetryEvent = New-TelemetryEvent -Event $Event -Properties $Properties -Metrics $Metrics -NickName $NickName Send-TelemetryObject -Data $telemetryEvent } } function Send-TelemetryEvents { param( [Parameter(Mandatory = $true)] [array]$Events ) process { Send-TelemetryObject -Data $Events } } function Read-TelemetryLevel { process { # Ask user for consent WriteInfoHighlighted "`nLab telemetry" WriteInfo "By providing a telemetry information you will help us to improve WSLab scripts. There are two levels of a telemetry information and we are not collecting any personally identifiable information (PII)." WriteInfo "Details about telemetry levels and the content of telemetry messages can be found in documentation https://aka.ms/wslab/telemetry" WriteInfo "Available telemetry levels are:" WriteInfo " * None -- No information will be sent" WriteInfo " * Basic -- Information about lab will be sent (e.g. script execution time, number of VMs, guest OSes)" WriteInfo " * Full -- Information about lab and the host machine (e.g. type of disk)" WriteInfo "Would you be OK with providing an information about your WSLab usage?" WriteInfo "`nTip: You can also configure telemetry settings explicitly in LabConfig.ps1 file or by setting an environmental variable and suppress this prompt." $options = [System.Management.Automation.Host.ChoiceDescription[]] @( <# 0 #> New-Object System.Management.Automation.Host.ChoiceDescription "&None", "No information will be sent" <# 1 #> New-Object System.Management.Automation.Host.ChoiceDescription "&Basic", "Lab info will be sent (e.g. script execution time, number of VMs)" <# 2 #> New-Object System.Management.Automation.Host.ChoiceDescription "&Full", "More details about the host machine and deployed VMs (e.g. guest OS)" ) $response = $host.UI.PromptForChoice("WSLab telemetry level", "Please choose a telemetry level for this WSLab instance. For more details please see WSLab documentation.", $options, 1 <#default option#>) $telemetryLevel = $null switch($response) { 0 { $telemetryLevel = 'None' WriteInfo "`nNo telemetry information will be sent." } 1 { $telemetryLevel = 'Basic' WriteInfo "`nTelemetry has been set to Basic level, thank you for your valuable feedback." } 2 { $telemetryLevel = 'Full' WriteInfo "`nTelemetry has been set to Full level, thank you for your valuable feedback." } } $telemetryLevel } } # Instance values $ScriptRoot = $PSScriptRoot $wslabVersion = "v21.09.3" $TelemetryEnabledLevels = "Basic", "Full" $TelemetryInstrumentationKey = "9ebf64de-01f8-4f60-9942-079262e3f6e0" $TelemetrySessionId = $ScriptRoot + $env:COMPUTERNAME + ((Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Cryptography).MachineGuid) | Get-StringHash #endregion Function CreateUnattendFileBlob{ #Create Unattend (parameter is Blob) param ( [parameter(Mandatory=$true)] [string] $Blob, [parameter(Mandatory=$true)] [string] $AdminPassword, [parameter(Mandatory=$true)] [string] $TimeZone, [parameter(Mandatory=$false)] [string] $RunSynchronous ) if ( Test-Path "$PSScriptRoot\Temp\unattend.xml" ) { Remove-Item "$PSScriptRoot\Temp\unattend.xml" } $unattendFile = New-Item "$PSScriptRoot\Temp\unattend.xml" -type File $fileContent = @" $Blob $AdminPassword true</PlainText> </AdministratorPassword> </UserAccounts> <OOBE> <HideEULAPage>true</HideEULAPage> <SkipMachineOOBE>true</SkipMachineOOBE> <SkipUserOOBE>true</SkipUserOOBE> </OOBE> <TimeZone>$TimeZone</TimeZone> </component> </settings> <settings pass="specialize"> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> $oeminformation <RegisteredOwner>PFE</RegisteredOwner> <RegisteredOrganization>PFE Inc.</RegisteredOrganization> </component> <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <RunSynchronous> $RunSynchronous </RunSynchronous> </component> </settings> </unattend> "@ Set-Content $unattendFile $fileContent #return the file object $unattendFile } Function CreateUnattendFileNoDjoin{ #Create Unattend(without domain join) param ( [parameter(Mandatory=$true)] [string] $ComputerName, [parameter(Mandatory=$true)] [string] $AdminPassword, [parameter(Mandatory=$true)] [string] $TimeZone, [parameter(Mandatory=$false)] [string] $RunSynchronous, [parameter(Mandatory=$false)] [string] $AdditionalAccount ) if ( Test-Path "$PSScriptRoot\Temp\unattend.xml" ) { Remove-Item "$PSScriptRoot\Temp\unattend.xml" } $unattendFile = New-Item "$PSScriptRoot\Temp\unattend.xml" -type File $fileContent = @" <?xml version='1.0' encoding='utf-8'?> <unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <settings pass="specialize"> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ComputerName>$Computername</ComputerName> $oeminformation <RegisteredOwner>PFE</RegisteredOwner> <RegisteredOrganization>PFE Inc.</RegisteredOrganization> </component> <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <RunSynchronous> $RunSynchronous </RunSynchronous> </component> </settings> <settings pass="oobeSystem"> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> <UserAccounts> $AdditionalAccount <AdministratorPassword> <Value>$AdminPassword</Value> <PlainText>true</PlainText> </AdministratorPassword> </UserAccounts> <OOBE> <HideEULAPage>true</HideEULAPage> <SkipMachineOOBE>true</SkipMachineOOBE> <SkipUserOOBE>true</SkipUserOOBE> </OOBE> <TimeZone>$TimeZone</TimeZone> </component> </settings> </unattend> "@ Set-Content $unattendFile $fileContent #return the file object $unattendFile } Function CreateUnattendFileWin2012{ #Create Unattend(traditional Djoin with username/pass) param ( [parameter(Mandatory=$true)] [string] $ComputerName, [parameter(Mandatory=$true)] [string] $AdminPassword, [parameter(Mandatory=$true)] [string] $TimeZone, [parameter(Mandatory=$false)] [string] $RunSynchronous, [parameter(Mandatory=$true)] [string] $DomainName ) if ( Test-Path "$PSScriptRoot\Temp\unattend.xml" ) { Remove-Item "$PSScriptRoot\Temp\unattend.xml" } $unattendFile = New-Item "$PSScriptRoot\Temp\unattend.xml" -type File $fileContent = @" <?xml version='1.0' encoding='utf-8'?> <unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <settings pass="specialize"> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ComputerName>$Computername</ComputerName> $oeminformation <RegisteredOwner>PFE</RegisteredOwner> <RegisteredOrganization>PFE Inc.</RegisteredOrganization> </component> <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <RunSynchronous> $RunSynchronous </RunSynchronous> </component> <component name="Microsoft-Windows-UnattendedJoin" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Identification> <Credentials> <Domain>$DomainName</Domain> <Password>$AdminPassword</Password> <Username>Administrator</Username> </Credentials> <JoinDomain>$DomainName</JoinDomain> </Identification> </component> </settings> <settings pass="oobeSystem"> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> <UserAccounts> <AdministratorPassword> <Value>$AdminPassword</Value> <PlainText>true</PlainText> </AdministratorPassword> </UserAccounts> <OOBE> <HideEULAPage>true</HideEULAPage> <SkipMachineOOBE>true</SkipMachineOOBE> <SkipUserOOBE>true</SkipUserOOBE> </OOBE> <TimeZone>$TimeZone</TimeZone> </component> </settings> </unattend> "@ Set-Content $unattendFile $fileContent #return the file object $unattendFile } Function AdditionalLocalAccountXML{ #Creates Additional local account unattend piece param ( [parameter(Mandatory=$true)] [string] $AdminPassword, [parameter(Mandatory=$true)] [string] $AdditionalAdminName ) @" <LocalAccounts> <LocalAccount wcm:action="add"> <Password> <Value>$AdminPassword</Value> <PlainText>true</PlainText> </Password> <Description>$AdditionalAdminName admin account</Description> <DisplayName>$AdditionalAdminName</DisplayName> <Group>Administrators</Group> <Name>$AdditionalAdminName</Name> </LocalAccount> </LocalAccounts> "@ } function Get-WindowsBuildNumber { $os = Get-CimInstance -ClassName Win32_OperatingSystem return [int]($os.BuildNumber) } Function Set-VMNetworkConfiguration { #source:http://www.ravichaganti.com/blog/?p=2766 with some changes #example use: Get-VMNetworkAdapter -VMName Demo-VM-1 -Name iSCSINet | Set-VMNetworkConfiguration -IPAddress 192.168.100.1 00 -Subnet 255.255.0.0 -DNSServer 192.168.100.101 -DefaultGateway 192.168.100.1 [CmdletBinding()] Param ( [Parameter(Mandatory=$true, Position=1, ParameterSetName='DHCP', ValueFromPipeline=$true)] [Parameter(Mandatory=$true, Position=0, ParameterSetName='Static', ValueFromPipeline=$true)] [Microsoft.HyperV.PowerShell.VMNetworkAdapter]$NetworkAdapter, [Parameter(Mandatory=$true, Position=1, ParameterSetName='Static')] [String[]]$IPAddress=@(), [Parameter(Mandatory=$false, Position=2, ParameterSetName='Static')] [String[]]$Subnet=@(), [Parameter(Mandatory=$false, Position=3, ParameterSetName='Static')] [String[]]$DefaultGateway = @(), [Parameter(Mandatory=$false, Position=4, ParameterSetName='Static')] [String[]]$DNSServer = @(), [Parameter(Mandatory=$false, Position=0, ParameterSetName='DHCP')] [Switch]$Dhcp ) $VM = Get-CimInstance -Namespace "root\virtualization\v2" -ClassName "Msvm_ComputerSystem" | Where-Object ElementName -eq $NetworkAdapter.VMName $VMSettings = Get-CimAssociatedInstance -InputObject $vm -ResultClassName "Msvm_VirtualSystemSettingData" | Where-Object VirtualSystemType -EQ "Microsoft:Hyper-V:System:Realized" $VMNetAdapters = Get-CimAssociatedInstance -InputObject $VMSettings -ResultClassName "Msvm_SyntheticEthernetPortSettingData" $networkAdapterConfiguration = @() foreach ($netAdapter in $VMNetAdapters) { if ($netAdapter.ElementName -eq $NetworkAdapter.Name) { $networkAdapterConfiguration = Get-CimAssociatedInstance -InputObject $netAdapter -ResultClassName "Msvm_GuestNetworkAdapterConfiguration" break } } $networkAdapterConfiguration.PSBase.CimInstanceProperties["IPAddresses"].Value = $IPAddress $networkAdapterConfiguration.PSBase.CimInstanceProperties["Subnets"].Value = $Subnet $networkAdapterConfiguration.PSBase.CimInstanceProperties["DefaultGateways"].Value = $DefaultGateway $networkAdapterConfiguration.PSBase.CimInstanceProperties["DNSServers"].Value = $DNSServer $networkAdapterConfiguration.PSBase.CimInstanceProperties["ProtocolIFType"].Value = 4096 if ($dhcp) { $networkAdapterConfiguration.PSBase.CimInstanceProperties["DHCPEnabled"].Value = $true } else { $networkAdapterConfiguration.PSBase.CimInstanceProperties["DHCPEnabled"].Value = $false } $cimSerializer = [Microsoft.Management.Infrastructure.Serialization.CimSerializer]::Create() $serializedInstance = $cimSerializer.Serialize($networkAdapterConfiguration, [Microsoft.Management.Infrastructure.Serialization.InstanceSerializationOptions]::None) $serializedInstanceString = [System.Text.Encoding]::Unicode.GetString($serializedInstance) $service = Get-CimInstance -ClassName "Msvm_VirtualSystemManagementService" -Namespace "root\virtualization\v2" $setIp = Invoke-CimMethod -InputObject $service -MethodName "SetGuestNetworkAdapterConfiguration" -Arguments @{ ComputerSystem = $VM NetworkConfiguration = @($serializedInstanceString) } if($setIp.ReturnValue -eq 0) { # completed WriteInfo "`t`t Success" } else { # unexpected response $setIp } } function WrapProcess{ #Using this function you can run legacy program and search in output string #Example: WrapProcess -filename fltmc.exe -arguments "attach svhdxflt e:" -outputstring "Success" [CmdletBinding()] [Alias()] [OutputType([bool])] Param ( # process name. For example fltmc.exe [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] $filename, # arguments. for example "attach svhdxflt e:" [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=1)] $arguments, # string to search. for example "attach svhdxflt e:" [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=1)] $outputstring ) Process { $procinfo = New-Object System.Diagnostics.ProcessStartInfo $procinfo.FileName = $filename $procinfo.Arguments = $arguments $procinfo.UseShellExecute = $false $procinfo.CreateNoWindow = $true $procinfo.RedirectStandardOutput = $true $procinfo.RedirectStandardError = $true # Create a process object using the startup info $process = New-Object System.Diagnostics.Process $process.StartInfo = $procinfo # Start the process $process.Start() | Out-Null # test if process is still running if(!$process.HasExited){ do{ Start-Sleep 1 }until ($process.HasExited -eq $true) } # get output $out = $process.StandardOutput.ReadToEnd() if ($out.Contains($outputstring)) { $output=$true } else { $output=$false } return, $output } } Function BuildVM { [cmdletbinding()] param( [PSObject]$VMConfig, [PSObject]$LabConfig, [string]$LabFolder ) WriteInfoHighlighted "Creating VM $($VMConfig.VMName)" WriteInfo "`t Looking for Parent Disk" $serverparent=Get-ChildItem "$PSScriptRoot\ParentDisks\" -Recurse | Where-Object Name -eq $VMConfig.ParentVHD if ($serverparent -eq $null){ WriteErrorAndExit "Server parent disk $($VMConfig.ParentVHD) not found." }else{ WriteInfo "`t`t Server parent disk $($serverparent.Name) found" } $VMname=$Labconfig.Prefix+$VMConfig.VMName if ($serverparent.Extension -eq ".vhdx"){ $vhdpath="$LabFolder\VMs\$VMname\Virtual Hard Disks\$VMname.vhdx" }elseif($serverparent.Extension -eq ".vhd"){ $vhdpath="$LabFolder\VMs\$VMname\Virtual Hard Disks\$VMname.vhd" } WriteInfoHighlighted "`t Creating OS VHD" New-VHD -ParentPath $serverparent.fullname -Path $vhdpath #Get VM Version [System.Version]$VMVersion=(Get-WindowsImage -ImagePath $VHDPath -Index 1).Version WriteInfo "`t VM Version is $($VMVersion.Build).$($VMVersion.Revision)" WriteInfo "`t Creating VM" if ($VMConfig.Generation -eq 1){ $VMTemp=New-VM -Name $VMname -VHDPath $vhdpath -MemoryStartupBytes $VMConfig.MemoryStartupBytes -path "$LabFolder\VMs" -SwitchName $SwitchName -Generation 1 }else{ $VMTemp=New-VM -Name $VMname -VHDPath $vhdpath -MemoryStartupBytes $VMConfig.MemoryStartupBytes -path "$LabFolder\VMs" -SwitchName $SwitchName -Generation 2 } $VMTemp | Set-VMMemory -DynamicMemoryEnabled $true $VMTemp | Get-VMNetworkAdapter | Rename-VMNetworkAdapter -NewName Management1 if ($VMTemp.AutomaticCheckpointsEnabled -eq $True){ $VMTemp | Set-VM -AutomaticCheckpointsEnabled $False } $MGMTNICs=$VMConfig.MGMTNICs If($MGMTNICs -eq $null){ $MGMTNICs = 2 } If($MGMTNICs -gt 8){ $MGMTNICs=8 } If($MGMTNICs -ge 2){ 2..$MGMTNICs | ForEach-Object { WriteInfo "`t Adding Network Adapter Management$_" $VMTemp | Add-VMNetworkAdapter -Name "Management$_" } } WriteInfo "`t Connecting vNIC to $switchname" $VMTemp | Get-VMNetworkAdapter | Connect-VMNetworkAdapter -SwitchName $SwitchName if ($LabConfig.Secureboot -eq $False) { WriteInfo "`t Disabling Secureboot" $VMTemp | Set-VMFirmware -EnableSecureBoot Off } if ($VMConfig.AdditionalNetworks -eq $True){ WriteInfo "`t Configuring Additional networks" foreach ($AdditionalNetworkConfig in $Labconfig.AdditionalNetworksConfig){ WriteInfo "`t`t Adding Adapter $($AdditionalNetworkConfig.NetName) with IP $($AdditionalNetworkConfig.NetAddress)$global:IP" $VMTemp | Add-VMNetworkAdapter -SwitchName $SwitchName -Name $AdditionalNetworkConfig.NetName $VMTemp | Get-VMNetworkAdapter -Name $AdditionalNetworkConfig.NetName | Set-VMNetworkConfiguration -IPAddress "$($AdditionalNetworkConfig.NetAddress)$global:IP" -Subnet $AdditionalNetworkConfig.Subnet if($AdditionalNetworkConfig.NetVLAN -ne 0){ $VMTemp | Get-VMNetworkAdapter -Name $AdditionalNetworkConfig.NetName | Set-VMNetworkAdapterVlan -VlanId $AdditionalNetworkConfig.NetVLAN -Access } } $global:IP++ } #Generate DSC Config if ($VMConfig.DSCMode -eq 'Pull'){ WriteInfo "`t Setting DSC Mode to Pull" PullClientConfig -ComputerName $VMConfig.VMName -DSCConfig $VMConfig.DSCConfig -OutputPath "$PSScriptRoot\temp\dscconfig" -DomainName $LabConfig.DomainName } #configure nested virt if ($VMConfig.NestedVirt -eq $True){ WriteInfo "`t Enabling NestedVirt" $VMTemp | Set-VMProcessor -ExposeVirtualizationExtensions $true $VMTemp | Set-VMMemory -DynamicMemoryEnabled $False } #configure vTPM if ($VMConfig.vTPM -eq $True){ if ($VMConfig.Generation -eq 1){ WriteError "`t vTPM requested. But vTPM is not compatible with Generation 1" }else{ WriteInfo "`t Enabling vTPM" $keyprotector = New-HgsKeyProtector -Owner $guardian -AllowUntrustedRoot Set-VMKeyProtector -VM $VMTemp -KeyProtector $keyprotector.RawData Enable-VMTPM -VM $VMTemp } } #set MemoryMinimumBytes if ($VMConfig.MemoryMinimumBytes -ne $null){ WriteInfo "`t Configuring MemoryMinimumBytes to $($VMConfig.MemoryMinimumBytes/1MB)MB" if ($VMConfig.NestedVirt){ "`t`t Skipping! NestedVirt configured" }else{ Set-VM -VM $VMTemp -MemoryMinimumBytes $VMConfig.MemoryMinimumBytes } } #Set static Memory if ($VMConfig.StaticMemory -eq $true){ WriteInfo "`t Configuring StaticMemory" $VMTemp | Set-VMMemory -DynamicMemoryEnabled $false } #configure number of processors if ($VMConfig.VMProcessorCount){ if ($VMConfig.VMProcessorCount -eq "Max"){ if ($NumberOfLogicalProcessors -gt 64){ WriteInfo "`t Processors Count $NumberOfLogicalProcessors and Max is specified. Configuring VM Processor Count to 64" $VMTemp | Set-VMProcessor -Count 64 }else{ WriteInfo "`t Configuring VM Processor Count to Max ($NumberOfLogicalProcessors)" $VMTemp | Set-VMProcessor -Count $NumberOfLogicalProcessors } }elseif ($VMConfig.VMProcessorCount -le $NumberOfLogicalProcessors){ WriteInfo "`t Configuring VM Processor Count to $($VMConfig.VMProcessorCount)" $VMTemp | Set-VMProcessor -Count $VMConfig.VMProcessorCount }else{ WriteError "`t`t Number of processors specified in VMProcessorCount is greater than Logical Processors available in Host!" WriteInfo "`t`t Number of logical Processors in Host $NumberOfLogicalProcessors" WriteInfo "`t`t Number of Processors provided in labconfig $($VMConfig.VMProcessorCount)" WriteInfo "`t`t Will configure maximum processors possible instead ($NumberOfLogicalProcessors)" $VMTemp | Set-VMProcessor -Count $NumberOfLogicalProcessors } }else{ $VMTemp | Set-VMProcessor -Count 2 } $Name=$VMConfig.VMName #add run synchronous commands WriteInfo "`t Adding Sync Commands" $RunSynchronous="" if ($VMConfig.EnableWinRM){ $RunSynchronous+=@' <RunSynchronousCommand wcm:action="add"> <Path>cmd.exe /c winrm quickconfig -q -force</Path> <Description>enable winrm</Description> <Order>1</Order> </RunSynchronousCommand> '@ WriteInfo "`t`t WinRM will be enabled" } if ($VMConfig.DisableWCF){ $RunSynchronous+=@' <RunSynchronousCommand wcm:action="add"> <Path>reg add HKLM\Software\Policies\Microsoft\Windows\CloudContent /v DisableWindowsConsumerFeatures /t REG_DWORD /d 1 /f</Path> <Description>disable consumer features</Description> <Order>2</Order> </RunSynchronousCommand> '@ WriteInfo "`t`t WCF will be disabled" } if ($VMConfig.CustomPowerShellCommands){ $Order=3 foreach ($CustomPowerShellCommand in $VMConfig.CustomPowerShellCommands){ $RunSynchronous+=@" <RunSynchronousCommand wcm:action="add"> <Path>powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -Command "$CustomPowerShellCommand"</Path> <Description>run custom powershell</Description> <Order>$Order</Order> </RunSynchronousCommand> "@ $Order++ } WriteInfo "`t`t Custom PowerShell command will be added" } if (-not $RunSynchronous){ WriteInfo "`t`t No sync commands requested" } if ($VMVersion.Build -ge 17763){ $oeminformation=@" <OEMInformation> <SupportProvider>MSLab</SupportProvider> <SupportURL>https://aka.ms/mslab</SupportURL> </OEMInformation> "@ }else{ $oeminformation=$null } #configure native VLAN and AllowedVLANs WriteInfo "`t Configuring NativeVLAN and AllowedVLANs" if ($VMConfig.ManagementSubnetID -gt 0){ $NativeVlanId=($HighestVLAN+$VMConfig.ManagementSubnetID) WriteInfo "`t`t Subnet ID is $($VMConfig.ManagementSubnetID) with NativeVLAN $NativeVLanID. AllowedVLANIDList is $($LabConfig.AllowedVLANs),$NativeVLANID" $VMTemp | Set-VMNetworkAdapterVlan -VMNetworkAdapterName "Management*" -Trunk -NativeVlanId $NativeVlanId -AllowedVlanIdList "$($LabConfig.AllowedVLANs),$NativeVLANID" }else{ WriteInfo "`t`t Subnet ID is 0 with NativeVLAN 0. AllowedVlanIDList is $($LabConfig.AllowedVLANs)" $VMTemp | Set-VMNetworkAdapterVlan -VMNetworkAdapterName "Management*" -Trunk -NativeVlanId 0 -AllowedVlanIdList "$($LabConfig.AllowedVLANs)" } #Create Unattend file if ($VMConfig.Unattend -eq "NoDjoin" -or $VMConfig.SkipDjoin){ WriteInfo "`t Skipping Djoin" if ($VMConfig.AdditionalLocalAdmin){ WriteInfo "`t Additional Local Admin $($VMConfig.AdditionalLocalAdmin) will be added" $AdditionalLocalAccountXML=AdditionalLocalAccountXML -AdditionalAdminName $VMConfig.AdditionalLocalAdmin -AdminPassword $LabConfig.AdminPassword $unattendfile=CreateUnattendFileNoDjoin -ComputerName $Name -AdminPassword $LabConfig.AdminPassword -RunSynchronous $RunSynchronous -AdditionalAccount $AdditionalLocalAccountXML -TimeZone $TimeZone }else{ $unattendfile=CreateUnattendFileNoDjoin -ComputerName $Name -AdminPassword $LabConfig.AdminPassword -RunSynchronous $RunSynchronous -TimeZone $TimeZone } }elseif($VMConfig.Win2012Djoin -or $VMConfig.Unattend -eq "DjoinCred"){ WriteInfoHighlighted "`t Creating Unattend with win2012-ish domain join" $unattendfile=CreateUnattendFileWin2012 -ComputerName $Name -AdminPassword $LabConfig.AdminPassword -DomainName $Labconfig.DomainName -RunSynchronous $RunSynchronous -TimeZone $TimeZone }elseif($VMConfig.Unattend -eq "DjoinBlob" -or -not ($VMConfig.Unattend)){ WriteInfoHighlighted "`t Creating Unattend with djoin blob" $path="c:\$vmname.txt" Invoke-Command -VMGuid $DC.id -Credential $cred -ScriptBlock {param($Name,$path,$Labconfig); djoin.exe /provision /domain $labconfig.DomainNetbiosName /machine $Name /savefile $path /machineou "OU=$($Labconfig.DefaultOUName),$($Labconfig.DN)"} -ArgumentList $Name,$path,$Labconfig $blob=Invoke-Command -VMGuid $DC.id -Credential $cred -ScriptBlock {param($path); get-content $path} -ArgumentList $path Invoke-Command -VMGuid $DC.id -Credential $cred -ScriptBlock {param($path); Remove-Item $path} -ArgumentList $path $unattendfile=CreateUnattendFileBlob -Blob $blob.Substring(0,$blob.Length-1) -AdminPassword $LabConfig.AdminPassword -RunSynchronous $RunSynchronous -TimeZone $TimeZone }elseif($VMConfig.Unattend -eq "None"){ $unattendFile=$Null } #adding unattend to VHD if ($unattendFile){ WriteInfo "`t Adding unattend to VHD" Mount-WindowsImage -Path $mountdir -ImagePath $VHDPath -Index 1 Use-WindowsUnattend -Path $mountdir -UnattendPath $unattendFile #&"$PSScriptRoot\Tools\dism\dism" /mount-image /imagefile:$vhdpath /index:1 /MountDir:$mountdir #&"$PSScriptRoot\Tools\dism\dism" /image:$mountdir /Apply-Unattend:$unattendfile New-item -type directory "$mountdir\Windows\Panther" -ErrorAction Ignore Copy-Item $unattendfile "$mountdir\Windows\Panther\unattend.xml" } if ($VMConfig.DSCMode -eq 'Pull'){ WriteInfo "`t Adding metaconfig.mof to VHD" Copy-Item "$PSScriptRoot\temp\dscconfig\$name.meta.mof" -Destination "$mountdir\Windows\system32\Configuration\metaconfig.mof" } if ($unattendFile){ Dismount-WindowsImage -Path $mountdir -Save #&"$PSScriptRoot\Tools\dism\dism" /Unmount-Image /MountDir:$mountdir /Commit } #add toolsdisk if ($VMConfig.AddToolsVHD -eq $True){ $VHD=New-VHD -ParentPath "$($toolsparent.fullname)" -Path "$LabFolder\VMs\$VMname\Virtual Hard Disks\tools.vhdx" WriteInfoHighlighted "`t Adding Virtual Hard Disk $($VHD.Path)" $VMTemp | Add-VMHardDiskDrive -Path $vhd.Path } # return info [PSCustomObject]@{ OSDiskPath = $vhdpath VM = $VMTemp } } #endregion #region Initialization Start-Transcript -Path "$PSScriptRoot\Deploy.log" $StartDateTime = Get-Date WriteInfoHighlighted "Script started at $StartDateTime" WriteInfo "`nMSLab Version $mslabVersion" ##Load LabConfig.... . "$PSScriptRoot\LabConfig.ps1" # Telemetry if(-not (Get-TelemetryLevel)) { $telemetryLevel = Read-TelemetryLevel $LabConfig.TelemetryLevel = $telemetryLevel $LabConfig.TelemetryLevelSource = "Prompt" $promptShown = $true } if((Get-TelemetryLevel) -in $TelemetryEnabledLevels) { if(-not $promptShown) { WriteInfo "Telemetry is set to $(Get-TelemetryLevel) level from $(Get-TelemetryLevelSource)" } Send-TelemetryEvent -Event "Deploy.Start" -NickName $LabConfig.TelemetryNickName | Out-Null } #endregion #region Set variables If (!$LabConfig.DomainNetbiosName){ $LabConfig.DomainNetbiosName="Corp" } If (!$LabConfig.DomainName){ $LabConfig.DomainName="Corp.contoso.com" } If (!$LabConfig.DefaultOUName){ $LabConfig.DefaultOUName="Workshop" } if (!$Labconfig.AllowedVLANs){ $Labconfig.AllowedVLANs="1-10" } $DN=$null $LabConfig.DomainName.Split(".") | ForEach-Object { $DN+="DC=$_," } $LabConfig.DN=$DN.TrimEnd(",") $global:IP=1 if (!$LabConfig.Prefix){ $labconfig.prefix="$($PSScriptRoot | Split-Path -Leaf)-" } if (!$LabConfig.SwitchName){ $LabConfig.SwitchName = 'LabSwitch' } WriteInfoHighlighted "List of variables used" WriteInfo "`t Prefix used in lab is $($labconfig.prefix)" $SwitchName=($labconfig.prefix+$LabConfig.SwitchName) WriteInfo "`t Switchname is $SwitchName" WriteInfo "`t Workdir is $PSScriptRoot" $LABfolder="$PSScriptRoot\LAB" WriteInfo "`t LabFolder is $LabFolder" $LABfolderDrivePath=$LABfolder.Substring(0,3) $ExternalSwitchName="$($Labconfig.Prefix)$($LabConfig.Switchname)-External" #Grab TimeZone $TimeZone=(Get-TimeZone).id #Grab number of processors Get-CimInstance -ClassName "win32_processor" | ForEach-Object { $global:NumberOfLogicalProcessors += $_.NumberOfLogicalProcessors } #Calculate highest VLAN (for additional subnets) [int]$HighestVLAN=$LabConfig.AllowedVLANs -split "," -split "-" | Select -Last 1 #Grab defined Management Subnet IDs and ignore 0 $ManagementSubnetIDs=$labconfig.vms.ManagementSubnetID | Select-Object -Unique | Sort-Object | Where-Object {$_ -ne 0} WriteInfo "`t Requested ManagementSubnetIDs: $ManagementSubnetIDs" #endregion #region Some Additional checks and prereqs configuration # Checking if not running in root folder if (($PSScriptRoot).Length -eq 3) { WriteErrorAndExit "`t MSLab canot run in root folder. Please put MSLab scripts into a folder. Exiting" } # Checking for Compatible OS WriteInfoHighlighted "Checking if OS is Windows 10 1511 (10586)/Server 2016 or newer" $BuildNumber=Get-WindowsBuildNumber if ($BuildNumber -ge 10586){ WriteSuccess "`t OS is Windows 10 1511 (10586)/Server 2016 or newer" }else{ WriteErrorAndExit "`t Windows 10/ Server 2016 not detected. Exiting" } # Checking for NestedVirt if ($LABConfig.VMs.NestedVirt -contains $True){ $BuildNumber=Get-WindowsBuildNumber if ($BuildNumber -ge 14393){ WriteSuccess "`t Windows is build greater than 14393. NestedVirt will work" }else{ WriteErrorAndExit "`t Windows build older than 14393 detected. NestedVirt will not work. Exiting" } } # Checking for vTPM support if ($LABConfig.VMs.vTPM -contains $true){ $BuildNumber=Get-WindowsBuildNumber if ($BuildNumber -ge 14393){ WriteSuccess "`t Windows is build greater than 14393. vTPM will work" }else{ WriteErrorAndExit "`t Windows build older than 14393 detected. vTPM will not work Exiting" } <# Not needed anymore as VBS is automatically enabled since 14393 when vTPM is used if (((Get-CimInstance -ClassName Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard).VirtualizationBasedSecurityStatus -ne 0) -and ((Get-Process "secure system") -ne $null )){ WriteSuccess "`t Virtualization Based Security is running. vTPM can be enabled" }else{ WriteErrorAndExit "`t Virtualization based security is not running. Enable VBS, or remove vTPM from configuration" } #> #load Guardian $guardian=Get-HgsGuardian | Select-Object -first 1 if($guardian -eq $null){ $guardian=New-HgsGuardian -Name LabGuardian -GenerateCertificates WriteInfo "`t HGS with name LabGuardian created" } } #Check support for shared disks + enable if possible if ($LABConfig.VMs.Configuration -contains "Shared" -or $LABConfig.VMs.Configuration -contains "Replica"){ WriteInfoHighlighted "Configuration contains Shared or Replica scenario" WriteInfo "Checking support for shared disks" $OS = Get-CimInstance -ClassName "win32_operatingsystem" if (($OS.caption -like "*Server*") -and $OS.version -gt 10){ WriteInfo "`t Installing Failover Clustering Feature" $FC=Install-WindowsFeature Failover-Clustering If ($FC.Success -eq $True){ WriteSuccess "`t`t Failover Clustering Feature installed with exit code: $($FC.ExitCode)" }else{ WriteError "`t`t Failover Clustering Feature was not installed with exit code: $($FC.ExitCode)" } } WriteInfoHighlighted "`t Attaching svhdxflt filter driver to drive $LABfolderDrivePath" if (WrapProcess -filename fltmc.exe -arguments "attach svhdxflt $LABfolderDrivePath" -outputstring "successful"){ WriteSuccess "`t Svhdx filter driver was successfully attached" }else{ if (WrapProcess -filename fltmc.exe -arguments "attach svhdxflt $LABfolderDrivePath" -outputstring "0x801f0012"){ WriteSuccess "`t Svhdx filter driver was already attached" }else{ WriteErrorAndExit "`t unable to load svhdx filter driver. Exiting Please use Server SKU or figure out how to install svhdx into the client SKU" } } WriteInfoHighlighted "Adding svhdxflt to registry for autostart" if (!(Test-Path HKLM:\SYSTEM\CurrentControlSet\Services\svhdxflt\Parameters)){ New-Item HKLM:\SYSTEM\CurrentControlSet\Services\svhdxflt\Parameters } New-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\svhdxflt\Parameters -Name AutoAttachOnNonCSVVolumes -PropertyType DWORD -Value 1 -force } #Check if Hyper-V is installed WriteInfoHighlighted "Checking if Hyper-V is installed" if ((Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V).state -eq "Enabled"){ WriteSuccess "`t Hyper-V is Installed" }else{ WriteErrorAndExit "`t Hyper-V not installed. Please install hyper-v feature including Hyper-V management tools. Exiting" } WriteInfoHighlighted "Checking if Hyper-V Powershell module is installed" if ((Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Management-PowerShell).state -eq "Enabled"){ WriteSuccess "`t Hyper-V is Installed" }else{ WriteErrorAndExit "`t Hyper-V tools are not installed. Please install Hyper-V management tools. Exiting" } #Check if at least 2GB (+200Mb just to be sure) memory is available WriteInfoHighlighted "Checking if at least 2GB RAM is available" $MemoryAvailableMB=(Get-Ciminstance Win32_OperatingSystem).FreePhysicalMemory/1KB if ($MemoryAvailableMB -gt (2048+200)){ WriteSuccess "`t $("{0:n0}" -f $MemoryAvailableMB) MB RAM Available" }else{ WriteErrorAndExit "`t Please make sure you have at least 2 GB available memory. Exiting" } #check if filesystem on volume is NTFS or ReFS WriteInfoHighlighted "Checking if volume filesystem is NTFS or ReFS" $driveletter=$PSScriptRoot -split ":" | Select-Object -First 1 if ($PSScriptRoot -like "c:\ClusterStorage*"){ WriteSuccess "`t Volume Cluster Shared Volume. Mountdir will be $env:Temp\MSLabMountdir" $mountdir="$env:Temp\MSLabMountDir" $VolumeFileSystem="CSVFS" }else{ $mountdir="$PSScriptRoot\Temp\MountDir" $VolumeFileSystem=(Get-Volume -DriveLetter $driveletter).FileSystemType if ($VolumeFileSystem -match "NTFS"){ WriteSuccess "`t Volume filesystem is $VolumeFileSystem" }elseif ($VolumeFileSystem -match "ReFS") { WriteSuccess "`t Volume filesystem is $VolumeFileSystem" }else { WriteErrorAndExit "`t Volume filesystem is $VolumeFileSystem. Must be NTFS or ReFS. Exiting" } } #enable EnableEnhancedSessionMode if not enabled if (-not (Get-VMHost).EnableEnhancedSessionMode){ WriteInfoHighlighted "Enhanced session mode was disabled. Enabling." Set-VMHost -EnableEnhancedSessionMode $true } #Create Switches WriteInfoHighlighted "Creating Switch" WriteInfo "`t Checking if $SwitchName already exists..." if (-not (Get-VMSwitch -Name $SwitchName -ErrorAction Ignore)){ WriteInfo "`t Creating $SwitchName..." if ($LabConfig.SwitchNICs){ #test if NICs are not already connected to another switch $VMSComponentStatus=Get-NetAdapterBinding -Name $LabConfig.SwitchNICs -ComponentID vms_pp if (($VMSComponentStatus).Enabled -contains $true){ $BoundNICs=$VMSComponentStatus | Where-Object Enabled -eq $true $InterfaceGUIDs=(Get-NetAdapter -Name $BoundNICs.Name).InterfaceGUID $vSwitches=ForEach ($InterfaceGUID in $InterfaceGUIDs){Get-VMSwitch | Where-Object NetAdapterInterfaceGuid -Contains $InterfaceGuid} WriteError "Following NICs are already bound to a Virtual Switch:" $BoundNICs WriteError "Virtual Switch list:" $vSwitches | Select-Object Name,NetAdapterInterfaceDescriptions WriteErrorAndExit "At least one NIC is connected to existing Virtual Switch, different than specified in Labconfig ($SwitchName)" }else{ New-VMSwitch -Name $SwitchName -EnableEmbeddedTeaming $true -EnableIov $true -NetAdapterName $LabConfig.SwitchNics -AllowManagementOS $False } }else{ New-VMSwitch -SwitchType Private -Name $SwitchName } }else{ $SwitchNameExists=$True WriteInfo "`t $SwitchName exists. Looks like lab with same prefix exists. " } #connect lab to internet if specified in labconfig if ($Labconfig.Internet){ WriteInfoHighlighted "Internet connectivity requested" if (!$LabConfig.CustomDnsForwarders){ $LabConfig.CustomDnsForwarders=@("8.8.8.8","1.1.1.1") # Google DNS, Cloudfare } WriteInfo "`t Detecting default vSwitch" $DefaultSwitch=Get-VMSwitch -ID c08cb7b8-9b3c-408e-8e30-5e16a3aeb444 -ErrorAction Ignore if ($DefaultSwitch){WriteInfo "`t Default switch detected"} #if running in Azure and default switch is not present, create InternalNAT Switch If ((Get-CimInstance win32_systemenclosure).SMBIOSAssetTag -eq "7783-7084-3265-9085-8269-3286-77" -and !$DefaultSwitch){ #https://docs.microsoft.com/en-us/azure/virtual-machines/windows/nested-virtualization#set-up-internet-connectivity-for-the-guest-virtual-machine WriteInfoHighlighted "`t Lab is running in Azure and default switch not detected" if (Get-VMSwitch -name "InternalNat" -ErrorAction Ignore){ WriteInfo "`t vSwitch InternalNat detected, skipping creation" $DefaultSwitch=Get-VMSwitch -Name "InternalNAT" }else{ WriteInfo "`t Creating vSwitch `"InternalNat`"" $DefaultSwitch=New-VMSwitch -Name "InternalNAT" -SwitchType Internal WriteInfo "`t Assigning IP address 192.168.0.1 to interface `"vEthernet (InternalNAT)`"" New-NetIPAddress -IPAddress 192.168.0.1 -PrefixLength 24 -InterfaceAlias "vEthernet (InternalNAT)" WriteInfo "`t Assigning IP address 192.168.0.1 to interface `"vEthernet (InternalNAT)`"" New-NetNat -Name "InternalNat" -InternalIPInterfaceAddressPrefix 192.168.0.0/24 } } if (-not $DefaultSwitch){ WriteInfo "`t Default switch not present, detecting external vSwitch $ExternalSwitchName" $ExternalSwitch=Get-VMSwitch -SwitchType External -Name $ExternalSwitchName -ErrorAction Ignore if ($ExternalSwitch){ WriteSuccess "`t External vSwitch $ExternalSwitchName detected" }else{ WriteInfo "`t Detecting external VMSwitch" $ExtSwitch=Get-VMSwitch -SwitchType External | Where-Object Name -NotLike $SwitchName if (!$ExtSwitch){ WriteInfoHighlighted "`t No External Switch detected. Will create one " $TempNetAdapters=get-netadapter | Where-Object Name -NotLike vEthernet* | Where-Object status -eq up if (!$TempNetAdapters){ WriteErrorAndExit "No Adapters with Status -eq UP detected. Exitting" } if ($TempNetAdapters.name.count -eq 1){ WriteInfo "`t Just one connected NIC detected ($($TempNetAdapters.name)). Will create vSwitch connected to it" $ExternalSwitch=New-VMSwitch -NetAdapterName $TempNetAdapters.name -Name $ExternalSwitchName -AllowManagementOS $true } if ($TempNetAdapters.name.count -gt 1){ WriteInfo "`t More than 1 NIC detected" WriteInfoHighlighted "`t Please select NetAdapter you want to use for vSwitch" $tempNetAdapter=get-netadapter | Where-Object Name -NotLike vEthernet* | Where-Object status -eq up | Out-GridView -OutputMode Single -Title "Please select adapter you want to use for External vSwitch" if (!$tempNetAdapter){ WriteErrorAndExit "You did not select any net adapter. Exitting." } $ExternalSwitch=New-VMSwitch -NetAdapterName $tempNetAdapter.name -Name $ExternalSwitchName -AllowManagementOS $true } } if ($ExtSwitch.count -eq 1){ WriteSuccess "`t External vswitch $($ExtSwitch.name) found. Will be used for connecting lab to internet" $ExternalSwitch=$ExtSwitch } if ($ExtSwitch.count -gt 1){ WriteInfoHighlighted "`t More than 1 External Switch found. Please chose what switch you want to use for internet connectivity" $ExternalSwitch=Get-VMSwitch -SwitchType External | Out-GridView -OutputMode Single -Title 'Please Select External Switch you want to use for Internet Connectivity' } } } } #Testing if lab already exists. WriteInfoHighlighted "Checking if lab already exists." $LABExists=$false if ($SwitchNameExists){ if ((Get-vm -Name ($labconfig.prefix+"DC") -ErrorAction SilentlyContinue) -ne $null){ $LABExists=$true WriteInfo "`t Lab already exists. If labconfig contains additional VMs, they will be added." }else{ WriteInfo "`t Lab does not exist, will be created" } } #If lab exists, correct starting IP will be calculated if (($LABExists) -and ($labconfig.AdditionalNetworksInDC)) { $global:IP++ } $Labconfig.VMs | ForEach-Object { if (((Get-VM -Name ($labconfig.prefix+$_.vmname) -ErrorAction SilentlyContinue) -ne $null) -and ($_.AdditionalNetworks -eq $True)){ $global:IP++ } } WriteInfo "`t Starting IP for AdditionalNetworks is $global:IP" #Create Mount nd VMs directories WriteInfoHighlighted "Creating Mountdir" New-Item $mountdir -ItemType Directory -Force WriteInfoHighlighted "Creating VMs dir" New-Item "$PSScriptRoot\LAB\VMs" -ItemType Directory -Force #get path for Tools disk WriteInfoHighlighted "Looking for Tools Parent Disks" $toolsparent=Get-ChildItem "$PSScriptRoot\ParentDisks" -Recurse | Where-Object name -eq tools.vhdx if ($toolsparent -eq $null){ WriteInfo "`t Tools parent disk not found. Will create one." WriteInfoHighlighted "Creating Tools.vhdx" $toolsVHD=New-VHD -Path "$PSScriptRoot\ParentDisks\tools.vhdx" -SizeBytes 30GB -Dynamic #mount and format VHD $VHDMount = Mount-VHD $toolsVHD.Path -Passthru $vhddisk = $VHDMount| get-disk $vhddisk | Initialize-Disk -PartitionStyle GPT -PassThru | New-Partition -UseMaximumSize -AssignDriveLetter |Format-Volume -FileSystem NTFS -AllocationUnitSize 8kb -NewFileSystemLabel ToolsDisk #dismount VHD Dismount-VHD $vhddisk.Number }else{ WriteInfo "`t Tools parent disk $($toolsparent.fullname) found" } #endregion #region Import DC (if not already present) or just grab it and start WriteInfoHighlighted "Configuring DC" $dcCandidate = (get-vm -Name ($labconfig.prefix+"DC") -ErrorAction SilentlyContinue) if($dcCandidate -and !$dcCandidate.ConfigurationLocation.StartsWith($LabFolder)) { WriteErrorAndExit "DC with name $($labconfig.prefix+"DC") already exists on this system in the different lab folder [$($dcCandidate.ConfigurationLocation)]..." } if (!(get-vm -Name ($labconfig.prefix+"DC") -ErrorAction SilentlyContinue)){ #import DC WriteInfo "`t Looking for DC to be imported" $dcCandidates = [array](Get-ChildItem $LABFolder -Recurse | Where-Object {($_.extension -eq '.vmcx' -and $_.directory -like '*Virtual Machines*') -or ($_.extension -eq '.xml' -and $_.directory -like '*Virtual Machines*')}) $dcCandidates | ForEach-Object -Process { # If the VM ID is already used create a copy of the DC VM configuration instead of in-place registration $vm = Get-VM -Id $_.BaseName -ErrorAction SilentlyContinue if($vm -and $dcCandidates.Length -eq 1) { # allow duplicating of the DC VM only if it is the only one VM in lab folder (as if more than one exists, probably just labprefix was changed after the deployment) WriteInfoHighlighted "You are trying to deploy a previously deployed lab from a different location as there is another DC VM with a same VM ID (is this a copied lab folder?) -> this DC VM will be registered with new VM ID." $directory = $_.Directory.FullName.replace("\Virtual Machines", "") $DC = Import-VM -Path $_.FullName -GenerateNewId -Copy -VirtualMachinePath $directory -VhdDestinationPath "$directory\Virtual Hard Disks" WriteInfo "`t`t Virtual Machine $($DC.Name) registered with a new VM ID $($DC.Id)" } else { $DC = Import-VM -Path $_.FullName } } if ($DC -eq $null){ WriteErrorAndExit "DC was not imported successfully Press any key to continue ..." }else{ WriteInfo "`t`t Virtual Machine $($DC.name) located in folder $($DC.Path) imported" } #create checkpoint to be able to return to consistent state when cleaned with cleanup.ps1 $DC | Checkpoint-VM -SnapshotName Initial WriteInfo "`t Virtual Machine $($DC.name) checkpoint created" Start-Sleep -Seconds 5 #rename network adapters and add another WriteInfo "`t Configuring Network" $DC | Get-VMNetworkAdapter | Rename-VMNetworkAdapter -NewName Management1 If($labconfig.MGMTNICsInDC -gt 8){ $labconfig.MGMTNICsInDC=8 } If($labconfig.MGMTNICsInDC -ge 2){ 2..$labconfig.MGMTNICsInDC | ForEach-Object { WriteInfo "`t Adding Network Adapter Management$_" $DC | Add-VMNetworkAdapter -Name Management$_ } } $DC | Get-VMNetworkAdapter | Connect-VMNetworkAdapter -SwitchName $SwitchName #add aditional networks if ($labconfig.AdditionalNetworksInDC -eq $True){ WriteInfo "`t Configuring Additional networks" foreach ($AdditionalNetworkConfig in $Labconfig.AdditionalNetworksConfig){ $DC | Add-VMNetworkAdapter -SwitchName $SwitchName -Name $AdditionalNetworkConfig.NetName WriteInfo "`t`t Adding Adapter $($AdditionalNetworkConfig.NetName) with IP $($AdditionalNetworkConfig.NetAddress)$global:IP" $DC | Get-VMNetworkAdapter -Name $AdditionalNetworkConfig.NetName | Set-VMNetworkConfiguration -IPAddress "$($AdditionalNetworkConfig.NetAddress)$global:IP" -Subnet $AdditionalNetworkConfig.Subnet if($AdditionalNetworkConfig.NetVLAN -ne 0){ $DC | Get-VMNetworkAdapter -Name $AdditionalNetworkConfig.NetName | Set-VMNetworkAdapterVlan -VlanId $AdditionalNetworkConfig.NetVLAN -Access } } $global:IP++ } #Enable VMNics device naming WriteInfo "`t Enabling DC VMNics device naming" $DC | Set-VMNetworkAdapter -DeviceNaming On #add tools disk WriteInfo "`t Adding Tools disk to DC machine" $VHD=New-VHD -ParentPath "$($toolsparent.fullname)" -Path "$LABFolder\VMs\ToolsDiskDC.vhdx" WriteInfo "`t`t Adding Virtual Hard Disk $($VHD.Path)" $DC | Add-VMHardDiskDrive -Path $vhd.Path #modify number of CPUs if ($Labconfig.DCVMProcessorCount){ WriteInfo "`t Configuring VM Processor Count for DC VM to $($labconfig.DCVMProcessorCount)" If ($labconfig.DCVMProcessorCount -le $NumberOfLogicalProcessors){ $DC | Set-VMProcessor -Count $Labconfig.DCVMProcessorCount }else{ WriteError "`t`t Number of processors specified in DCVMProcessorCount is greater than Logical Processors available in Host!" WriteInfo "`t`t Number of logical Processors in Host $NumberOfLogicalProcessors" WriteInfo "`t`t Number of Processors provided in labconfig $($labconfig.DCVMProcessorCount)" WriteInfo "`t`t Will configure maximum processors possible instead ($NumberOfLogicalProcessors)" $DC | Set-VMProcessor -Count $NumberOfLogicalProcessors } } #start DC WriteInfo "`t Starting Virtual Machine $($DC.name)" $DC | Start-VM #rename DC VM WriteInfo "`t Renaming $($DC.name) to $($labconfig.Prefix+$DC.name)" $DC | Rename-VM -NewName ($labconfig.Prefix+$DC.name) }else{ #if DC was present, just grab it $DC=get-vm -Name ($labconfig.prefix+"DC") } #add addtional subnets if specified if ($ManagementSubnetIDs){ WriteInfo "`t Adding adapters for additional subnets" foreach ($number in $ManagementSubnetIDs){ if ($DC | Get-VMNetworkadapter -Name "Subnet$number" -ErrorAction Ignore){ WriteInfo "`t`t Adapters Subnet$number already present" }else{ WriteInfo "`t`t Adding adapter Subnet$number and configuring Access VLANID $($HighestVLAN+$number)" $DC | Add-VMNetworkAdapter -SwitchName $SwitchName -Name "Subnet$number" -DeviceNaming On $DC | Set-VMNetworkAdapterVlan -VMNetworkAdapterName "Subnet$number" -Access -VlanId ($HighestVLAN+$number) } } } #Start DC if it is not running if ($DC.State -ne "Running"){ WriteInfo "DC was not started. Starting now..." $DC | Start-VM } #connect to internet if ($labconfig.internet){ if (-not ($DC | Get-VMNetworkAdapter -Name Internet -ErrorAction SilentlyContinue)){ WriteInfo "`t Adding Network Adapter Internet" $DC | Add-VMNetworkAdapter -Name Internet -DeviceNaming On if ($DefaultSwitch){ $internetSwitch = $DefaultSwitch }else{ $internetSwitch = $ExternalSwitch } WriteInfo "`t Connecting Network Adapter Internet to $($internetSwitch.Name)" $DC | Get-VMNetworkAdapter -Name Internet | Connect-VMNetworkAdapter -VMSwitch $internetSwitch } } #connect add VLAN to internet adapter if requested if ($Labconfig.Internet -and $LabConfig.InternetVLAN){ WriteInfo "`t Configuring Internet VLAN for DC" $DC | Get-VMNetworkAdapter -Name Internet | Set-VMNetworkAdapterVlan -Access -VlanId $LabConfig.InternetVLAN } #endregion #region Test DC to come up #Credentials for Session $username = "$($Labconfig.DomainNetbiosName)\Administrator" $password = $LabConfig.AdminPassword $secstr = New-Object -TypeName System.Security.SecureString $password.ToCharArray() | ForEach-Object {$secstr.AppendChar($_)} $cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $secstr #wait for DC to start WriteInfoHighlighted "`t Waiting for Active Directory on $($DC.name) to be Started." do{ $test=Invoke-Command -VMGuid $DC.id -Credential $cred -ArgumentList $Labconfig -ErrorAction Ignore -ScriptBlock { param($labconfig); Get-ADComputer -Filter * -SearchBase "$($LabConfig.DN)" -ErrorAction Ignore } Start-Sleep 5 }until ($test -ne $Null) WriteSuccess "`t Active Directory on $($DC.name) is up." #if DC was just created, configure additional settings with PowerShell direct if (!$LABExists){ WriteInfoHighlighted "`t Performing some actions against DC with powershell Direct" #Configure IP address on Internet NIC for Windows Server on Azure if ($DefaultSwitch.Name -eq "InternalNAT"){ $startIP=(Get-VMNetworkAdapter -ManagementOS -SwitchName "InternalNat").Count+1 $IP="192.168.0.$startIP" WriteInfo "`t Configure static IP $IP on Internet NIC" Invoke-Command -VMGuid $DC.id -Credential $cred -ScriptBlock { $NetAdapterName=(Get-NetAdapterAdvancedProperty | where displayvalue -eq Internet).Name New-NetIPAddress -InterfaceAlias $NetAdapterName -IPAddress $using:IP -PrefixLength 24 -DefaultGateway "192.168.0.1" } } #make tools disk online WriteInfo "`t Making tools disk online" Invoke-Command -VMGuid $DC.id -Credential $cred -ScriptBlock {get-disk | Where-Object operationalstatus -eq offline | Set-Disk -IsReadOnly $false} Invoke-Command -VMGuid $DC.id -Credential $cred -ScriptBlock {get-disk | Where-Object operationalstatus -eq offline | Set-Disk -IsOffline $false} #authorize DHCP (if more networks added, then re-authorization is needed. Also if you add multiple networks once, it messes somehow even with parent VM for DC) WriteInfo "`t Authorizing DHCP" Invoke-Command -VMGuid $DC.id -Credential $cred -ArgumentList $Labconfig -ScriptBlock { param($labconfig); Get-DhcpServerInDC | Remove-DHCPServerInDC Add-DhcpServerInDC -DnsName "DC.$($Labconfig.DomainName)" -IPAddress 10.0.0.1 } } #configure NAT on DC If ($labconfig.internet){ $cmd=Invoke-Command -VMGuid $DC.id -Credential $cred -ScriptBlock {Get-WindowsFeature -Name Routing} if ($cmd.installed -eq $False){ WriteInfoHighlighted "`t Configuring NAT on DC" WriteInfo "`t Installing Routing and RSAT-RemoteAccess features" $cmd=Invoke-Command -VMGuid $DC.id -Credential $cred -ScriptBlock { Install-WindowsFeature -Name Routing,RSAT-RemoteAccess -IncludeAllSubFeature -WarningAction Ignore } if ($cmd.restartneeded -eq "Yes"){ WriteInfo "`t Restart of DC is requested" WriteInfo "`t Restarting DC" $DC | Restart-VM -Force Start-Sleep 10 WriteInfoHighlighted "`t Waiting for Active Directory on $($DC.name) to be Started." do{ $test=Invoke-Command -VMGuid $DC.id -Credential $cred -ArgumentList $Labconfig -ErrorAction SilentlyContinue -ScriptBlock { param($labconfig); Get-ADComputer -Filter * -SearchBase "$($LabConfig.DN)" -ErrorAction SilentlyContinue } Start-Sleep 5 }until ($test -ne $Null) WriteSuccess "`t Active Directory on $($DC.name) is up." } $DNSServers=@() if($LabConfig.UseHostDnsAsForwarder){ WriteInfo "`t Requesting DNS settings from Host" if($internetSwitch.Name -eq "Default Switch"){ # Host's IP of Default Switch acts also as DNS resolver $DNSServers+=(Get-HnsNetwork | Where-Object { $_.Name -eq "Default Switch" }).Subnets[0].GatewayAddress } else{ $vNICName=(Get-VMNetworkAdapter -ManagementOS -SwitchName $internetSwitch.Name).Name | Select-Object -First 1 #in case multiple adapters are in managementos $DNSServers+=(Get-NetIPConfiguration -InterfaceAlias "vEthernet ($vNICName)").DNSServer.ServerAddresses #grab DNS IP from vNIC } } $DNSServers+=$LabConfig.CustomDnsForwarders WriteInfo "`t Configuring NAT with netSH and starting services" Invoke-Command -VMGuid $DC.id -Credential $cred -ScriptBlock { Set-Service -Name RemoteAccess -StartupType Automatic Start-Service -Name RemoteAccess netsh.exe routing ip nat install netsh.exe routing ip nat add interface (Get-NetAdapterAdvancedProperty | Where-Object displayvalue -eq "Internet").Name netsh.exe routing ip nat set interface (Get-NetAdapterAdvancedProperty | Where-Object displayvalue -eq "Internet").Name mode=full netsh.exe ras set conf confstate = enabled netsh.exe routing ip dnsproxy install Write-Output "`t Restarting service RemoteAccess..." Restart-Service -Name RemoteAccess -WarningAction SilentlyContinue Add-DnsServerForwarder $Using:DNSServers } } } #configure NICs and routing if ManagementSubnetIDs are specified if ($ManagementSubnetIDs){ WriteInfoHighlighted "`t Configuring subnets in DC" #configure static IPs on SubnetX adapters Invoke-Command -VMGuid $DC.id -Credential $cred -ScriptBlock { Foreach ($number in $using:ManagementSubnetIDs){ $IP="10.0.$number.1" $AdapterName="Subnet$Number" $NetAdapterName=(Get-NetAdapterAdvancedProperty | where displayvalue -eq $AdapterName).Name if (Get-NetIPAddress -InterfaceAlias $NetAdapterName -IPAddress $IP -ErrorAction Ignore){ Write-Output "`t`t Subnet $AdapterName already configured" }else{ Write-Output "`t`t Configuring static IP address $IP on Adapter $NetAdapterName" New-NetIPAddress -InterfaceAlias $NetAdapterName -IPAddress $IP -PrefixLength 24 #add dhcp scope Write-Output "`t`t Adding DHCP Scope ID 10.0.$number.0 and it's DHCP options" Add-DhcpServerv4Scope -StartRange "10.0.$number.10" -EndRange "10.0.$number.254" -Name "Scope$number" -State Active -SubnetMask 255.255.255.0 Set-DhcpServerv4OptionValue -OptionId 6 -Value "10.0.0.1" -ScopeId "10.0.$number.0" Set-DhcpServerv4OptionValue -OptionId 3 -Value "10.0.1.1" -ScopeId "10.0.$number.0" Set-DhcpServerv4OptionValue -OptionId 15 -Value "$($using:Labconfig.DomainName)" -ScopeId "10.0.$number.0" } } #make sure RRAS features are installed Write-Output "`t`t Making sure routing features are installed" Install-WindowsFeature -Name Routing,RSAT-RemoteAccess -IncludeAllSubFeature -WarningAction Ignore #enable routing Write-Output "`t`t Making sure routing is enabled" $routingEnabled = (Get-ItemProperty HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters -Name IPEnableRouter).IPEnableRouter if ($routingEnabled -match "0") { New-ItemProperty HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters -Name IPEnableRouter -value 1 -Force } #restart routing... just to make sure Write-Output "`t`t Restarting service RemoteAccess" Restart-Service RemoteAccess } } #endregion #region Provision VMs $vmDeploymentEvents = @() #DSC config for LCM (in case Pull configuration is specified) WriteInfoHighlighted "Creating DSC config to configure DC as pull server" [DSCLocalConfigurationManager()] Configuration PullClientConfig { param ( [Parameter(Mandatory=$true)] [string[]]$ComputerName, [Parameter(Mandatory=$true)] [string[]]$DSCConfig, [Parameter(Mandatory=$true)] [string[]]$DomainName ) Node $ComputerName { Settings{ AllowModuleOverwrite = $True ConfigurationMode = 'ApplyAndAutoCorrect' RefreshMode = 'Pull' RebootNodeIfNeeded = $True ActionAfterReboot = 'ContinueConfiguration' } ConfigurationRepositoryWeb PullServerWeb { ServerURL = "http://dc.$($DomainName):8080/PSDSCPullServer.svc" AllowUnsecureConnection = $true RegistrationKey = '14fc8e72-5036-4e79-9f89-5382160053aa' ConfigurationNames = $DSCConfig } ReportServerWeb PullServerReports { ServerURL = "http://dc.$($DomainName):8080/PSDSCPullServer.svc" RegistrationKey = '14fc8e72-5036-4e79-9f89-5382160053aa' } $DSCConfig | ForEach-Object { PartialConfiguration $_ { RefreshMode = 'Pull' ConfigurationSource = '[ConfigurationRepositoryWeb]PullServerWeb' } } } } #process $labconfig.VMs and create VMs (skip if machine already exists) WriteInfoHighlighted 'Processing $LabConfig.VMs, creating VMs' $provisionedVMs = @() foreach ($VMConfig in $LABConfig.VMs.GetEnumerator()){ if (!(Get-VM -Name "$($labconfig.prefix)$($VMConfig.vmname)" -ErrorAction SilentlyContinue)){ $vmProvisioningStartTime = Get-Date # Ensure that Configuration is set and use Simple as default if(-not $VMConfig.configuration) { $VMConfig.configuration = "Simple" } # Ensure that MemoryStartupBytes is set to use 512MB as default if(-not $VMConfig.MemoryStartupBytes) { $VMConfig.MemoryStartupBytes = 512MB } #create VM with Shared configuration if ($VMConfig.configuration -eq 'Shared'){ #create disks (if not already created) $VMSet=$VMConfig.VMSet if (!(Test-Path -Path "$LABfolder\VMs\*$VMSet*.VHDS")){ $SharedSSDs=$null $SharedHDDs=$null If (($VMConfig.SSDNumber -ge 1) -and ($VMConfig.SSDNumber -ne $null)){ $SharedSSDs= 1..$VMConfig.ssdnumber | ForEach-Object {New-vhd -Path "$LABfolder\VMs\SharedSSD-$VMSet-$_.VHDS" -Dynamic -Size $VMConfig.SSDSize} $SharedSSDs | ForEach-Object {WriteInfo "`t Disk SSD $($_.path) size $($_.size /1GB)GB created"} } If (($VMConfig.HDDNumber -ge 1) -and ($VMConfig.HDDNumber -ne $null)){ $SharedHDDs= 1..$VMConfig.hddnumber | ForEach-Object {New-VHD -Path "$LABfolder\VMs\SharedHDD-$VMSet-$_.VHDS" -Dynamic -Size $VMConfig.HDDSize} $SharedHDDs | ForEach-Object {WriteInfo "`t Disk HDD $($_.path) size $($_.size /1GB)GB created"} } }else{ $SharedSSDs=Get-VHD -Path "$LABfolder\VMs\SharedSSD-$VMSet-*.VHDS" -ErrorAction SilentlyContinue $SharedHDDs=Get-VHD -Path "$LABfolder\VMs\SharedHDD-$VMSet-*.VHDS" -ErrorAction SilentlyContinue } #Build VM $createdVm = BuildVM -VMConfig $VMConfig -LabConfig $labconfig -LabFolder $LABfolder #Compose VMName $VMname=$Labconfig.Prefix+$VMConfig.VMName #Add disks WriteInfoHighlighted "`t Attaching Shared Disks to $VMname" $SharedSSDs | ForEach-Object { $filename=$_.Path.Substring($_.Path.LastIndexOf("\")+1,$_.Path.Length-$_.Path.LastIndexOf("\")-1) Add-VMHardDiskDrive -Path $_.path -VMName $VMname -SupportPersistentReservations WriteInfo "`t`t $filename size $($_.size /1GB)GB added to $VMname" } $SharedHDDs | ForEach-Object { $filename=$_.Path.Substring($_.Path.LastIndexOf("\")+1,$_.Path.Length-$_.Path.LastIndexOf("\")-1) Add-VMHardDiskDrive -Path $_.Path -VMName $VMname -SupportPersistentReservations WriteInfo "`t`t $filename size $($_.size /1GB)GB added to $VMname" } } #create VM with Simple configuration if ($VMConfig.configuration -eq 'Simple'){ $createdVm = BuildVM -VMConfig $($VMConfig) -LabConfig $labconfig -LabFolder $LABfolder } #create VM with S2D configuration if ($VMConfig.configuration -eq 'S2D'){ #build VM $createdVm = BuildVM -VMConfig $VMConfig -LabConfig $labconfig -LabFolder $LABfolder #compose VM name $VMname=$Labconfig.Prefix+$VMConfig.VMName #Add disks #add "SSDs" If (($VMConfig.SSDNumber -ge 1) -and ($VMConfig.SSDNumber -ne $null)){ $SSDs= 1..$VMConfig.SSDNumber | ForEach-Object { New-vhd -Path "$LabFolder\VMs\$VMname\Virtual Hard Disks\SSD-$_.VHDX" -Dynamic -Size $VMConfig.SSDSize} WriteInfoHighlighted "`t Adding Virtual SSD Disks" $SSDs | ForEach-Object { $filename=$_.Path.Substring($_.Path.LastIndexOf("\")+1,$_.Path.Length-$_.Path.LastIndexOf("\")-1) Add-VMHardDiskDrive -Path $_.path -VMName $VMname WriteInfo "`t`t $filename size $($_.size /1GB)GB added to $VMname" } } #add "HDDs" If (($VMConfig.HDDNumber -ge 1) -and ($VMConfig.HDDNumber -ne $null)) { $HDDs= 1..$VMConfig.HDDNumber | ForEach-Object { New-VHD -Path "$LabFolder\VMs\$VMname\Virtual Hard Disks\HDD-$_.VHDX" -Dynamic -Size $VMConfig.HDDSize} WriteInfoHighlighted "`t Adding Virtual HDD Disks" $HDDs | ForEach-Object { $filename=$_.Path.Substring($_.Path.LastIndexOf("\")+1,$_.Path.Length-$_.Path.LastIndexOf("\")-1) Add-VMHardDiskDrive -Path $_.path -VMName $VMname WriteInfo "`t`t $filename size $($_.size /1GB)GB added to $VMname" } } } #create VM with Replica configuration if ($VMConfig.configuration -eq 'Replica'){ #create shared drives if not already created $VMSet=$VMConfig.VMSet if (!(Test-Path -Path "$LABfolder\VMs\*$VMSet*.VHDS")){ $ReplicaHDD= New-vhd -Path "$LABfolder\VMs\ReplicaHDD-$VMSet.VHDS" -Dynamic -Size $VMConfig.ReplicaHDDSize $ReplicaHDD | ForEach-Object {WriteInfo "`t`t ReplicaHDD $($_.path) size $($_.size /1GB)GB created"} $ReplicaLog= New-vhd -Path "$LABfolder\VMs\ReplicaLog-$VMSet.VHDS" -Dynamic -Size $VMConfig.ReplicaLogSize $ReplicaLog | ForEach-Object {WriteInfo "`t`t ReplicaLog $($_.path) size $($_.size /1GB)GB created"} }else{ $ReplicaHDD=Get-VHD -Path "$LABfolder\VMs\ReplicaHDD-$VMSet.VHDS" $ReplicaLog=Get-VHD -Path "$LABfolder\VMs\ReplicaLog-$VMSet.VHDS" } #build VM $createdVm = BuildVM -VMConfig $VMConfig -LabConfig $labconfig -LabFolder $LABfolder #Add disks $VMname=$Labconfig.Prefix+$VMConfig.VMName WriteInfoHighlighted "`t Attaching Shared Disks..." #Add HDD $ReplicaHdd | ForEach-Object { $filename=$_.Path.Substring($_.Path.LastIndexOf("\")+1,$_.Path.Length-$_.Path.LastIndexOf("\")-1) Add-VMHardDiskDrive -Path $_.path -VMName $VMname -SupportPersistentReservations WriteInfo "`t`t $filename size $($_.size /1GB)GB added to $VMname" } #add Log Disk $ReplicaLog | ForEach-Object { $filename=$_.Path.Substring($_.Path.LastIndexOf("\")+1,$_.Path.Length-$_.Path.LastIndexOf("\")-1) Add-VMHardDiskDrive -Path $_.Path -VMName $VMname -SupportPersistentReservations WriteInfo "`t`t $filename size $($_.size /1GB)GB added to $VMname" } } # Telemetry Report if((Get-TelemetryLevel) -in $TelemetryEnabledLevels) { $properties = @{ 'vm.configuration' = $VMConfig.Configuration 'vm.unattend' = $VMConfig.Unattend } if(Test-Path -Path $createdVm.OSDiskPath) { $osInfo = Get-WindowsImage -ImagePath $createdVm.OSDiskPath -Index 1 $properties.'vm.os.installationType' = $osInfo.InstallationType $properties.'vm.os.editionId' = $osInfo.EditionId $properties.'vm.os.version' = $osInfo.Version } $metrics = @{ 'vm.deploymentDuration' = ((Get-Date) - $vmProvisioningStartTime).TotalSeconds } $vmInfo = New-TelemetryEvent -Event "Deploy.VM" -Properties $properties -Metrics $metrics -NickName $LabConfig.TelemetryNickName $vmDeploymentEvents += $vmInfo } $provisionedVMs += $createdVm.VM } } #endregion #region Finishing WriteInfoHighlighted "Finishing..." #a bit cleanup Remove-Item -Path "$PSScriptRoot\temp" -Force -Recurse #set MacSpoofing and AllowTeaming (for SET switch in VMs to work properly with vNICs) WriteInfo "`t Setting MacSpoofing On and AllowTeaming On" Set-VMNetworkAdapter -VMName "$($labconfig.Prefix)*" -MacAddressSpoofing On -AllowTeaming On #list VMs $AllVMs = Get-VM | Where-Object name -like "$($labconfig.Prefix)*" $AllVMs | ForEach-Object { WriteSuccess "Machine $($_.VMName) provisioned" } #configure HostResourceProtection on all VM CPUs WriteInfo "`t Configuring EnableHostResourceProtection on all VM processors" Set-VMProcessor -EnableHostResourceProtection $true -VMName "$($labconfig.Prefix)*" -ErrorAction SilentlyContinue #Enable Guest services on all VMs if integration component if configured if ($labconfig.EnableGuestServiceInterface){ WriteInfo "`t Enabling Guest Service Interface" $vms = Get-VM -VMName "$($labconfig.Prefix)*" | Where-Object {$_.state -eq "Running" -or $_.state -eq "Off"} foreach ($vm in $vms) { $guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f $vm.Id $guestService = $vm | Get-VMIntegrationService | Where-Object -FilterScript {$_.Id -eq $guestServiceId} $guestService | Enable-VMIntegrationService } $TempVMs=Get-VM -VMName "$($labconfig.Prefix)*" | Where-Object {$_.state -ne "Running" -and $_.state -ne "Off"} if ($TempVMs){ WriteInfoHighlighted "`t`t Following VMs cannot be configured, as the state is not running or off" $TempVMs.Name } } #Enable VMNics device naming WriteInfo "`t Enabling VMNics device naming" Get-VM -VMName "$($labconfig.Prefix)*" | Where-Object Generation -eq 2 | Set-VMNetworkAdapter -DeviceNaming On #Autostart VMs $startVMs = 0 if($LabConfig.AutoStartAfterDeploy -eq $true -or $LabConfig.AutoStartAfterDeploy -eq "All") { $startVMs = 1 } elseif($LabConfig.AutoStartAfterDeploy -eq "DeployedOnly") { $startVMs = 2 } $CheckPointTime=Get-Date if(-not $LabConfig.ContainsKey("AutoStartAfterDeploy") -and $AllVMs.Count -gt 0) { $options = [System.Management.Automation.Host.ChoiceDescription[]] @( <# 0 #> New-Object System.Management.Automation.Host.ChoiceDescription "&No", "No VM will be started." <# 1 #> New-Object System.Management.Automation.Host.ChoiceDescription "&All", "All VMs in the lab will be started." ) if($provisionedVMs.Count -gt 0) { <# 2 #> $options += New-Object System.Management.Automation.Host.ChoiceDescription "&Deployed only", "Only newly deployed VMs will be started." } $startVMs = $host.UI.PromptForChoice("Start VMs", "Would you like to start lab virtual machines?", $options, 0 <#default option#>) } #Starting VMs $toStart = @() switch($startVMs) { 1 { $toStart = $AllVMs } 2 { $toStart = $provisionedVMs } } if(($toStart | Measure-Object).Count -gt 0) { WriteInfoHighlighted "Starting VMs" $toStart | ForEach-Object { WriteInfo "`t $($_.Name)" Start-VM -VM $_ -WarningAction SilentlyContinue } } # Telemetry Event if((Get-TelemetryLevel) -in $TelemetryEnabledLevels) { WriteInfo "Sending telemetry info" $metrics = @{ 'script.duration' = [Math]::Round(($CheckPointTime - $StartDateTime).TotalSeconds, 2) 'lab.vmsCount.active' = ($AllVMs | Measure-Object).Count # how many VMs are running 'lab.vmsCount.provisioned' = ($provisionedVMs | Measure-Object).Count # how many VMs were created by this script run } $properties = @{ 'lab.internet' = [bool]$LabConfig.Internet 'lab.isncrementalDeployment' = $LABExists 'lab.autostartmode' = $startVMs } $telemetryEvent = New-TelemetryEvent -Event "Deploy.End" -Metrics $metrics -Properties $properties -NickName $LabConfig.TelemetryNickName $vmDeploymentEvents += $telemetryEvent Send-TelemetryEvents -Events $vmDeploymentEvents | Out-Null } #write how much it took to deploy WriteInfo "Script finished at $CheckPointTime and took $(($CheckPointTime - $StartDateTime).TotalMinutes) Minutes" Stop-Transcript WriteSuccess "Press enter to continue ..." Read-Host | Out-Null #endregion