# 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
true
true
true
$TimeZone
$oeminformation
PFE
PFE Inc.
$RunSynchronous
"@
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 = @"
$Computername
$oeminformation
PFE
PFE Inc.
$RunSynchronous
$AdditionalAccount
$AdminPassword
true
true
true
true
$TimeZone
"@
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 = @"
$Computername
$oeminformation
PFE
PFE Inc.
$RunSynchronous
$DomainName
$AdminPassword
Administrator
$DomainName
$AdminPassword
true
true
true
true
$TimeZone
"@
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
)
@"
$AdminPassword
true
$AdditionalAdminName admin account
$AdditionalAdminName
Administrators
$AdditionalAdminName
"@
}
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+=@'
cmd.exe /c winrm quickconfig -q -force
enable winrm
1
'@
WriteInfo "`t`t WinRM will be enabled"
}
if ($VMConfig.DisableWCF){
$RunSynchronous+=@'
reg add HKLM\Software\Policies\Microsoft\Windows\CloudContent /v DisableWindowsConsumerFeatures /t REG_DWORD /d 1 /f
disable consumer features
2
'@
WriteInfo "`t`t WCF will be disabled"
}
if ($VMConfig.CustomPowerShellCommands){
$Order=3
foreach ($CustomPowerShellCommand in $VMConfig.CustomPowerShellCommands){
$RunSynchronous+=@"
powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -Command "$CustomPowerShellCommand"
run custom powershell
$Order
"@
$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=@"
MSLab
https://aka.ms/mslab
"@
}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