public/New-OSDWorkspaceVM.ps1
function New-OSDWorkspaceVM { <# .SYNOPSIS Creates a customized Hyper-V virtual machine from selected OSDWorkspace WinPE build media. .DESCRIPTION The New-OSDWorkspaceVM function creates a Hyper-V virtual machine that boots from the selected OSDWorkspace WinPE build media. This VM can be used for testing WinPE deployments, scripts, and other OSD tools in a virtualized environment. This function performs the following operations: 1. Validates that Hyper-V is available on the system 2. Prompts for selection of a WinPE build using Select-OSDWSWinPEBuild 3. Prompts for selection of a Media type (WinPE-Media or WinPE-MediaEX) 4. Creates a new Hyper-V VM with specified parameters 5. Configures secure boot, TPM, and other settings based on the VM generation 6. Mounts the selected ISO to the VM's DVD drive 7. Optionally creates an initial checkpoint 8. Optionally starts the VM The VM is highly customizable with options for memory, CPU, networking, display resolution, and storage configuration. .PARAMETER CheckpointVM Specifies whether to create a checkpoint of the VM after creation. Default value is $true. .PARAMETER Generation Specifies the Hyper-V VM generation to create. Valid values are 1 or 2. Default value is 2. Generation 1 VMs support legacy BIOS, while Generation 2 VMs support UEFI and secure boot. .PARAMETER MemoryStartupGB Specifies the amount of startup memory for the VM in gigabytes. Default value is 4. .PARAMETER NamePrefix Specifies a prefix for the VM name. The actual VM name will be in the format "NamePrefix-yyyy-MM-dd-HHmmss". Default value is 'OSDWS'. .PARAMETER ProcessorCount Specifies the number of virtual processors to allocate to the VM. Default value is 2. .PARAMETER DisplayResolution Specifies the display resolution of the VM. Valid values include '1024x768', '1280x720', '1280x768', '1280x800', '1280x960', '1280x1024', '1360x768', '1366x768', '1600x900', '1600x1200', '1680x1050', '1920x1080', and '1920x1200'. Default value is '1280x720'. .PARAMETER StartVM Specifies whether to start the VM after creation. Default value is $true. .PARAMETER SwitchName Specifies the name of the virtual switch to connect the VM to. Default value is 'Default Switch'. .PARAMETER VHDSizeGB Specifies the size of the virtual hard disk in gigabytes. Default value is 20. .EXAMPLE New-OSDWorkspaceVM Creates a Hyper-V VM with default settings (Generation 2, 4GB RAM, 2 CPUs, 20GB VHD), prompting for WinPE build selection. .EXAMPLE New-OSDWorkspaceVM -NamePrefix 'TestDeploy' -MemoryStartupGB 8 -ProcessorCount 4 -VHDSizeGB 50 Creates a customized Hyper-V VM with the name prefix 'TestDeploy', 8GB of RAM, 4 processors, and a 50GB virtual hard disk. .EXAMPLE New-OSDWorkspaceVM -CheckpointVM $false -Generation 2 -MemoryStartupGB 8 -NamePrefix 'MyVM' -ProcessorCount 4 -DisplayResolution '1920x1080' -StartVM $false -SwitchName 'MySwitch' -VHDSizeGB 50 Creates a Generation 2 Hyper-V VM with 8GB of memory, 4 processors, 1920x1080 display resolution, and a 50GB VHD. The VM will not be started automatically and will not have an initial checkpoint created. .OUTPUTS Microsoft.HyperV.PowerShell.VirtualMachine Returns the created Hyper-V virtual machine object. .NOTES Author: David Segura Version: 1.0 Date: April 29, 2025 Prerequisites: - PowerShell 5.0 or higher - Windows 10/11 Pro, Enterprise, or Server with Hyper-V role installed - Run as Administrator - At least one WinPE build available in the OSDWorkspace This function requires the Hyper-V PowerShell module to be installed and available. #> [CmdletBinding()] param ( # Specifies whether to create a checkpoint of the VM after creation. Default is $true. [System.Boolean] $CheckpointVM = $true, # Specifies the generation of the VM. Default is 2. [ValidateSet('1','2')] [System.UInt16] $Generation = 2, # Specifies the amount of memory in whole number GB to allocate to the VM. Default is 10. Maximum is 64. [ValidateRange(2, 64)] [System.UInt16] $MemoryStartupGB = 10, # Specifies the prefix to use for the VM name. Default is 'OSDWorkspace'. Full VM name will be in the format 'yyMMdd-HHmm 'NamePrefix' MediaName'. [System.String] $NamePrefix = 'OSDWorkspace', # Specifies the number of processors to allocate to the VM. Default is 2. Maximum is 64. [ValidateRange(2, 64)] [System.UInt16] $ProcessorCount = 2, # Specifies the display resolution of the VM. Default is '1440x900'. [ValidateSet('640x480','800x600','1024x768','1152x864','1280x720', '1280x768','1280x800','1280x960','1280x1024','1360x768','1366x768', '1400x1050','1440x900','1600x900','1680x1050','1920x1080','1920x1200', '2560x1440','2560x1600','3840x2160','3840x2400','4096x2160')] [System.String] $DisplayResolution = '1600x900', # Specifies whether to start the VM after creation. Default is $true. [System.Boolean] $StartVM = $true, # Specifies the name of the virtual switch to connect the VM to. If not specified, an Out-GridView will be displayed to select a switch. [System.String] $SwitchName, # Specifies the size of the VHD in whole number GB. Default is 64. Maximum is 128. [ValidateRange(8, 128)] [System.UInt16] $VHDSizeGB = 64 ) #================================================= $Error.Clear() Write-Verbose "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Start" Initialize-OSDWorkspace #================================================= # Requires Run as Administrator $IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $IsAdmin ) { Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] This function must be Run as Administrator" return } #================================================= # Is Hyper-V enabled? if (Test-IsHyperVEnabled) { Write-Verbose "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Hyper-V is enabled" } else { Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Hyper-V is not enabled, or may not be compatible with this version of Windows. Try running the following elevated Admin command:" Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Enable-WindowsOptionalFeature -Online -FeatureName 'Microsoft-Hyper-V-All' -NoRestart" return } #================================================= # Can only make a VM matching the architecture of the running OS $Architecture = $Env:PROCESSOR_ARCHITECTURE Write-Verbose "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Architecture = $Architecture" #================================================= # Do we have a Boot Media? $SelectWinPEBuild = $null $SelectWinPEBuild = Select-OSDWSWinPEBuild -Architecture $Architecture Write-Verbose "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] SelectWinPEBuild: $SelectWinPEBuild" if ($null -eq $SelectWinPEBuild) { Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] No OSDWorkspace WinPE Build was found or selected" return } #================================================= # Get Hyper-V Defaults #$VMManagementService = Get-WmiObject -Namespace root\virtualization\v2 Msvm_VirtualSystemManagementService $VMManagementServiceSettingData = Get-WmiObject -Namespace root\virtualization\v2 Msvm_VirtualSystemManagementServiceSettingData #================================================= # Set the Boot ISO Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Select a OSDWorkspace WinPE Build ISO to use with this Virtual Machine (Cancel to exit)" $SelectDvdDrive = Get-ChildItem "$($SelectWinPEBuild.Path)\iso" *.iso | Sort-Object Name, FullName | Select-Object Name, FullName | Out-GridView -Title 'Select a OSDWorkspace WinPE Build ISO to use with this Virtual Machine' -OutputMode Single if ($null -eq $SelectDvdDrive) { Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] No OSDWorkspace WinPE Build ISO was found" return } $DvdDrivePath = $SelectDvdDrive.FullName #================================================= # Select a Default Switch if (-not ($SwitchName)) { $GetVMSwitch = Get-VMSwitch -ErrorAction SilentlyContinue if ($GetVMSwitch.Count -ge 1) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Virtual Switch Name was not specified with the SwitchName parameter" $SwitchName = Get-VMSwitch | Select-Object Name, SwitchType, Id | Out-GridView -Title 'Select a Virtual Switch to use with this Virtual Machine (Cancel = Not connected)' -OutputMode Single | Select-Object -ExpandProperty Name Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] SwitchName: $SwitchName" } else { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] No Virtual Switches found with Get-VMSwitch, you will have to create a Virtual Switch. Setting to Not connected" $SwitchName = $null } } #================================================= # Automatically Set VM Name if ($SelectWinPEBuild.Name) { $VmName = "$((Get-Date).ToString('yyMMdd-HHmm')) $NamePrefix $($SelectWinPEBuild.Name)" } else { $VmName = "$((Get-Date).ToString('yyMMdd-HHmm')) $NamePrefix" } #================================================= # Set the Display Resolution #================================================= # Set Variables $VHDPath = [System.String](Join-Path $VMManagementServiceSettingData.DefaultVirtualHardDiskPath "$VmName.vhdx") $VHDSizeBytes = ($VHDSizeGB * 1GB) $VHDSizeGB = [System.Int64]$VHDSizeGB Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] MemoryStartupGB: $MemoryStartupGB" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] The -MemoryStartupGB parameter is used to specify the amount of memory in GB to allocate to the VM" $MemoryStartupBytes = ($MemoryStartupGB * 1GB) #================================================= # Create VM VHD Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] New-VM: Creating Virtual Machine in (5 second delay)" Start-Sleep -Seconds 5 if ($SwitchName) { $vm = New-VM -Name $VmName -Generation $Generation -MemoryStartupBytes $MemoryStartupBytes -NewVHDPath $VHDPath -NewVHDSizeBytes $VHDSizeBytes -SwitchName $SwitchName -ErrorAction Stop } else { $vm = New-VM -Name $VmName -Generation $Generation -MemoryStartupBytes $MemoryStartupBytes -NewVHDPath $VHDPath -NewVHDSizeBytes $VHDSizeBytes -ErrorAction Stop } #================================================= # Add DVD Drive Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Add-VMDvdDrive -Path $DvdDrivePath" $DvdDrive = $vm | Add-VMDvdDrive -Path $DvdDrivePath -Passthru Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Get-VMHardDiskDrive" $HardDiskDrive = $vm | Get-VMHardDiskDrive Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Get-VMNetworkAdapter" $NetworkAdapter = $vm | Get-VMNetworkAdapter #================================================= # Generation if ($Generation -eq 2) { # First Boot Device Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Set-VMFirmware -FirstBootDevice" $vm | Set-VMFirmware -FirstBootDevice $DvdDrive # Firmware #$vm | Set-VMFirmware -BootOrder $DvdDrive, $vmHardDiskDrive, $vmNetworkAdapter -Verbose # Security Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Set-VMFirmware -EnableSecureBoot On" $vm | Set-VMFirmware -EnableSecureBoot On if ((Get-TPM).TpmPresent -eq $true -and (Get-TPM).TpmReady -eq $true) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Set-VMSecurity -VirtualizationBasedSecurityOptOut:`$false" $vm | Set-VMSecurity -VirtualizationBasedSecurityOptOut:$false Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Set-VMKeyProtector -NewLocalKeyProtector" $vm | Set-VMKeyProtector -NewLocalKeyProtector Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Enable-VMTPM" $vm | Enable-VMTPM } } #================================================= # Memory Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Set-VMMemory -DynamicMemoryEnabled False" $vm | Set-VMMemory -DynamicMemoryEnabled $false #================================================= # Processor Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] ProcessorCount: $ProcessorCount" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] The -ProcessorCount parameter is used to set the number of processors to allocate to the VM. Default is 2" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Set-VMProcessor -Count $($ProcessorCount)" $vm | Set-VMProcessor -Count $ProcessorCount #================================================= # Display Resolution Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] DisplayResolution: $DisplayResolution" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] The -DisplayResolution parameter is used to set the resolution of the Virtual Machine. Default is 1440x900" $HorizontalResolution = $DisplayResolution.Split('x')[0] $VerticalResolution = $DisplayResolution.Split('x')[1] Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Set-VMVideo -HorizontalResolution $($HorizontalResolution) -VerticalResolution $($VerticalResolution) -ResolutionType Single" $vm | Set-VMVideo -HorizontalResolution $HorizontalResolution -VerticalResolution $VerticalResolution -ResolutionType Single #================================================= # Integration Services # Thanks Andreas Landry Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Enable-VMIntegrationService" $IntegrationService = Get-VMIntegrationService -VMName $vm.Name | Where-Object { $_ -match 'Microsoft:[0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12}\\6C09BB55-D683-4DA0-8931-C9BF705F6480' } $vm | Get-VMIntegrationService -Name $IntegrationService.Name | Enable-VMIntegrationService #================================================= # Checkpoints Start Stop Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Set-VM -AutomaticCheckpointsEnabled `$false -AutomaticStartAction Nothing -AutomaticStartDelay 3 -AutomaticStopAction Shutdown" $vm | Set-VM -AutomaticCheckpointsEnabled $false -AutomaticStartAction Nothing -AutomaticStartDelay 3 -AutomaticStopAction Shutdown #================================================= # Create a Snapshot if ($CheckpointVM -eq $true) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Checkpoint-VM -SnapshotName 'New-VM'" $vm | Checkpoint-VM -SnapshotName 'New-VM' } #================================================= # Export Final Configuration Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Exporting current configuration to $($SelectWinPEBuild.Path)\vm.json" $vm | ConvertTo-Json -Depth 5 | Out-File -FilePath "$($SelectWinPEBuild.Path)\vm.json" -Force #================================================= # Start VM if ($StartVM -eq $true) { Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] vmconnect.exe `"$($env:ComputerName) $VmName`"" vmconnect.exe $env:ComputerName $VmName Start-Sleep -Seconds 10 Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Start-VM" $vm | Start-VM } #================================================= Write-Verbose "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] End" #================================================= } Register-ArgumentCompleter -CommandName New-OSDWorkspaceVM -ParameterName 'SwitchName' -ScriptBlock {Get-VMSwitch | Select-Object -ExpandProperty Name | ForEach-Object {if ($_.Contains(' ')) {"'$_'"} else {$_}}} |