Functions/LabVM/New-LabVM.ps1
#Requires -Version 5.0 function New-LabVM { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'TODO: implement ShouldProcess')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'TODO: implement ShouldProcess')] [CmdletBinding(DefaultParameterSetName = 'MachineName')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Machine', ValueFromPipeline = $true)] [LabMachine[]]$Machine, [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'MachineName')] [string[]]$MachineName, [Parameter(Mandatory = $true, ParameterSetName = 'Environment', ValueFromPipeline = $true)] [LabEnvironment[]]$Environment, [Parameter(Mandatory = $false)] [Switch]$Force = $false, [Parameter(Mandatory = $false)] [Switch]$Start = $false ) Begin { if (-not (Test-Administrator)) { throw 'Please run this command as Administrator.' } } Process { if ($($PSCmdlet.ParameterSetName) -eq 'MachineName') { if ($MachineName) { $Machine = Get-LabMachine -Name $MachineName } else { $Machine = Get-LabMachine } } elseif ($($PSCmdlet.ParameterSetName) -eq 'Environment') { $Machine = Get-LabMachine -Environment $Environment } if (-not $Machine) { return } #region Validation foreach ($m in $Machine) { if (-not $m.Name) { throw 'Please provide Name for each machine.' } # if dynamic memory is to be used, both minimum & maximum must be provided if ((!$m.Hardware.MinimumMemory -and $m.Hardware.MaximumMemory) -or ($m.Hardware.MinimumMemory -and !$m.Hardware.MaximumMemory)) { throw "Invalid Dynamic Memory for machine '$($m.Name)'" } # TODO: improve validate of the machine-configuration <#if ($InternalSwitchName -and !(Get-VMSwitch | Where { $_.Name -eq $InternalSwitchName })) { throw "A switch with name '$InternalSwitchName' was not found." } if ($ExternalSwitchName -and !(Get-VMSwitch | Where { $_.Name -eq $ExternalSwitchName })) { throw "A switch with name '$ExternalSwitchName' was not found." }#> } #endregion #region delete existing VM Remove-LabVM -Machine $Machine -Force:$Force #-RemoveFromDomain #endregion foreach ($m in $Machine) { Write-Verbose -Message 'Starting creation of new lab-VM' $labPath = Split-Path -Path $m.Environment.Path -Parent # create new VM according to configuration Write-Verbose -Message "Creating lab-VM '$($m.Name)'" # determine path if ($m.Environment.MachinesPath) { $machinesPath = $m.Environment.MachinesPath if ($machinesPath.StartsWith('.')) { $machinesPath = [System.IO.Path]::GetFullPath((Join-Path -Path $labPath -ChildPath $machinesPath)) } } else { $machinesPath = [System.IO.Path]::Combine($labPath, 'Machines') } Write-Verbose -Message '- creating new VM' try { $vm = New-VM -Name $m.Name -Path $machinesPath -MemoryStartupBytes $m.Hardware.StartupMemory -Generation 2 -ErrorAction Stop } catch { Write-Warning "if New-VM fails due to 'Logon failure: the user has not been granted the requested logon type at this computer'" Write-Warning "execute 'Restart-Service Winmgmt -Force', and retry" Write-Error "Failed to create VM." return } Write-Verbose -Message '- configuring processors' Set-VMProcessor -VM $vm -Count $m.Hardware.ProcessorCount Write-Verbose -Message '- configuring memory' [bool]$dynamicMemoryEnabled = $m.Hardware.MinimumMemory Set-VMMemory -VM $vm -DynamicMemoryEnabled $dynamicMemoryEnabled -MinimumBytes $m.Hardware.MinimumMemory -MaximumBytes $m.Hardware.MaximumMemory -StartupBytes $m.Hardware.StartupMemory Write-Verbose -Message '- configuring snapshots and paging' $pathSnapshots = [System.IO.Path]::Combine($machinesPath, $vm.Name, 'Snapshots') $pathPaging = [System.IO.Path]::Combine($machinesPath, $vm.Name, 'Paging') Set-VM -VM $vm -SnapshotFileLocation $pathSnapshots -SmartPagingFilePath $pathPaging if ($m.Disks -and @($m.Disks).Length) { Write-Verbose -Message '- adding disk(s)' $pathHDDs = [System.IO.Path]::Combine($machinesPath, $vm.Name, 'Virtual Hard Disks') if(!(Test-Path -Path $pathHDDs)) { [System.IO.Directory]::CreateDirectory($pathHDDs) | Out-Null } $index = 0 foreach ($disk in $m.Disks) { if ($disk.Type -eq [LabDiskType]::OperatingSystem) { if ($disk.OperatingSystem.FilePath.StartsWith('.')) { $osPath = [System.IO.Path]::GetFullPath((Join-Path -Path $labPath -ChildPath $disk.OperatingSystem.FilePath)) } else { $osPath = $disk.OperatingSystem.FilePath } $diskPath = [System.IO.Path]::Combine($pathHDDs, "$($vm.Name).vhdx") Write-Verbose -Message " - creating OS disk at '$diskPath' from '$($osPath)'" if ($disk.DifferencingDisk) { if ($disk.UseEnvironmentCopy) { $diskParentPath = [System.IO.Path]::Combine($machinesPath, '_BaseDisks', [System.IO.Path]::GetFileName($osPath)) Write-Verbose -Message " - ensuring base-disk for differencing disk ($diskParentPath)" if (-not (Test-Path -Path $diskParentPath)) { New-Item -Path ([System.IO.Path]::GetDirectoryName($diskParentPath)) -ItemType Directory -ErrorAction SilentlyContinue | Out-Null Copy-Item -Path $osPath -Destination $diskParentPath } } else { $diskParentPath = $osPath } Write-Verbose -Message ' - creating differencing disk' New-VHD -Path $diskPath -ParentPath $diskParentPath -Differencing | Out-Null } else { Write-Verbose -Message ' - copying disk' Copy-Item -Path $osPath -Destination $diskPath } Write-Verbose -Message " - adding disk ($index)" Add-VMHardDiskDrive -VM $vm -ControllerType SCSI -ControllerNumber 0 -ControllerLocation $index -Path $diskPath $index++ } elseif ($disk.Type -eq [LabDiskType]::HardDisk) { $diskPath = [System.IO.Path]::Combine($pathHDDs, "$($vm.Name)_$index.vhdx") Write-Verbose -Message " - creating new disk with size '$($disk.Size / 1GB)GB' at '$diskPath' $(if ($disk.Shared) { '(shared)' })" New-VHD -Path $diskPath –SizeBytes $disk.Size | Out-Null Write-Verbose -Message " - adding disk ($index)" Add-VMHardDiskDrive -VM $vm -ControllerType SCSI -ControllerNumber 0 -ControllerLocation $index -Path $diskPath -SupportPersistentReservations:$disk.Shared $index++ } elseif ($disk.Type -eq [LabDiskType]::DVDDrive) { if ($disk.ImageFilePath) { Add-VMDvdDrive -VM $vm -ControllerNumber 0 -ControllerLocation $index -Path $disk.ImageFilePath } else { Add-VMDvdDrive -VM $vm -ControllerNumber 0 -ControllerLocation $index } $index++ } } } if ($m.NetworkAdapters -and @($m.NetworkAdapters).Length) { Write-Verbose -Message '- configuring network' Get-VMNetworkAdapter -VM $vm | Remove-VMNetworkAdapter foreach($networkAdapter in $m.NetworkAdapters) { if ($null -eq $networkAdapter.Enabled -or $networkAdapter.Enabled) { $network = $networkAdapter.Network # ensure VM-Switch exists, with correct settings $switch = Get-VMSwitch -Name $network.SwitchName -ErrorAction SilentlyContinue if ($switch) { if ($network.SwitchType -eq 'External') { $netAdapter = Get-NetAdapter -InterfaceDescription $switch.NetAdapterInterfaceDescription if ($switch.SwitchType -ne $network.SwitchType -or $netAdapter.Name -ne $network.SwitchNetAdapterName) { Set-VMSwitch -Name $network.SwitchName -NetAdapterName $network.SwitchNetAdapterName } } elseif ($switch.SwitchType -ne $network.SwitchType) { Set-VMSwitch -Name $network.SwitchName -SwitchType $network.SwitchType } } else { if ($network.SwitchType -eq 'External') { $switch = New-VMSwitch -Name $network.SwitchName -NetAdapterName $network.SwitchNetAdapterName } else { $switch = New-VMSwitch -Name $network.SwitchName -SwitchType $network.SwitchType } } if ($network.HostIPAddress) { $interfaceAlias = "vEthernet ($($network.SwitchName))" $netIPAddress = Get-NetAdapter $interfaceAlias | Get-NetIPAddress -AddressFamily IPv4 -IPAddress $network.HostIPAddress -ErrorAction SilentlyContinue if (-not $netIPAddress) { New-NetIPAddress –InterfaceAlias $interfaceAlias –IPAddress $network.HostIPAddress –PrefixLength $network.PrefixLength } elseif ($netIPAddress.PrefixLength -ne $network.PrefixLength) { Set-NetIPAddress –InterfaceAlias $interfaceAlias -PrefixLength $network.PrefixLength } } Write-Verbose -Message " - adding network '$($network.Name)'" Add-VMNetworkAdapter -VM $vm -Name $network.Name -SwitchName $network.SwitchName if ([System.Environment]::OSVersion.Version.Major -ge 10) { Set-VMNetworkAdapter -VM $vm -Name $network.Name -DeviceNaming On } if ($networkAdapter.StaticMacAddress) { Set-VMNetworkAdapter -VM $vm -Name $network.Name -StaticMacAddress ($networkAdapter.StaticMacAddress).Replace('-', '') } } else { Write-Verbose -Message " - skipping network '$($networkAdapter.Network.Name)'" } } } $pathOSDisk = (Get-VMHardDiskDrive -VM $vm ` | Where-Object { [System.IO.Path]::GetFileNameWithoutExtension($_.Path) -eq $m.Name } ` | Sort-Object ControllerNumber,ControllerLocation ` | Select-Object -First 1).Path if ($pathOSDisk) { Write-Verbose -Message '- fixing boot-order to OS-disk' Update-BootOrder -VM $vm -BootDrivePath $pathOSDisk $operatingSystem = ($m.Disks | Where-Object { $_.OperatingSystem }).OperatingSystem $unattendTemplateFilePath = $operatingSystem.UnattendFilePath if ($unattendTemplateFilePath) { Write-Verbose -Message '- generating unattend file' if ($unattendTemplateFilePath.StartsWith('.')) { $unattendTemplateFilePath = [System.IO.Path]::GetFullPath((Join-Path -Path $labPath -ChildPath $unattendTemplateFilePath)) } $administratorPassword = $null if ($m.AdministratorPassword) { $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($m.AdministratorPassword) $plain = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) $administratorPassword = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes("$($plain)AdministratorPassword")) } $unattendContent = New-UnattendXml -TemplateFilePath $unattendTemplateFilePath -Property @{ ComputerName = $m.Name ProductKey = $operatingSystem.ProductKey TimeZone = $m.TimeZone AdministratorPassword = $administratorPassword } } Write-Verbose -Message '- inserting files into virtual harddisk' $configurationContent = (ConvertTo-Json -InputObject $m.ToMachineConfiguration() -Depth 9) $filesToCopy = @( @{ Content = $unattendContent Destination = 'unattend.xml' } @{ Content = $configurationContent Destination = 'Setup\configuration.json' } @{ Source = "$PSScriptRoot\..\..\Files\*" Destination = '' } ) if ($m.Environment.FilesPath) { if ($m.Environment.FilesPath.StartsWith('.')) { $filesPath = [System.IO.Path]::GetFullPath((Join-Path -Path $labPath -ChildPath $m.Environment.FilesPath)) } else { $filesPath = $m.Environment.FilesPath } $filesToCopy += @{ Source = "$filesPath\*" Destination = '' } } if ($m.FilesPath) { if ($m.FilesPath.StartsWith('.')) { $filesPath = [System.IO.Path]::GetFullPath((Join-Path -Path $labPath -ChildPath $m.FilesPath)) } else { $filesPath = $m.FilesPath } $filesToCopy += @{ Source = "$filesPath\*" Destination = '' } } Add-FilesIntoVirtualHardDisk -Path $pathOSDisk -FilesToCopy $filesToCopy -ErrorAction Stop } if ($Start) { Write-Verbose -Message "Starting lab-VM '$($m.Name)'" Start-VM -Name $m.Name } } $environments = $Machine.Environment | Select-Object -Unique if ($environments) { foreach ($e in $environments) { if ($e.Host -and $e.Host.Share) { Update-LabHostShare -Environment $e } } } Write-Verbose -Message "Finished creating lab-VM '$($m.Name)'." } } |