public/New-OSDeployHyperVM.ps1
|
#Requires -PSEdition Core #Requires -Version 7.4 function New-OSDeployHyperVM { <# .SYNOPSIS Creates a Hyper-V virtual machine for OSDeploy workflows .DESCRIPTION Creates a timestamp-named Hyper-V VM with a new VHDX, fixed startup memory, configurable processors and display resolution, and a DVD drive. When ISO is omitted, the function selects the most recently modified bootmedia.iso under %ProgramData%\OSDeployCore\boot; when none exists, it creates an empty DVD drive. When SwitchName is omitted, the command selects Default Switch, then the first available switch, or creates an unconnected VM. Generation 2 VMs boot from the DVD drive with the selected Secure Boot template and enable available TPM-related settings when the host TPM is present and ready. The command can create an initial checkpoint and can invoke VMConnect before starting the VM. .PARAMETER ISO Specifies an existing file with an .iso extension. An empty or omitted value selects the newest OSDeploy bootmedia.iso, or leaves the new DVD drive empty when none is found. .PARAMETER NamePrefix Specifies text appended after a yyMMdd-HHmmss timestamp in the VM name. The default is OSDeploy. .PARAMETER Generation Specifies Hyper-V generation 1 or 2. The default is 2. .PARAMETER MemoryStartupGB Specifies fixed startup memory in GB from 2 through 64. The default is 8. .PARAMETER ProcessorCount Specifies 1 through 64 virtual processors. The default is 2. .PARAMETER VHDSizeGB Specifies the new VHDX size in GB from 8 through 512. The default is 64. .PARAMETER DisplayResolution Specifies one of the validated Hyper-V display resolutions. The default is 1600x900. .PARAMETER SwitchName Specifies an existing Hyper-V virtual-switch name. When omitted, the command uses Default Switch, then the first returned switch, or no switch when none is available. .PARAMETER SecureBootTemplate Specifies MicrosoftWindows, MicrosoftUEFICertificateAuthority, or OpenSourceShieldedVM for a Generation 2 VM. The default is MicrosoftWindows. Generation 1 ignores this value. .PARAMETER CheckpointVM Specifies whether to create a checkpoint named New-OSDeployHyperVM. The default is $true. .PARAMETER StartVM Specifies whether to invoke VMConnect when available and start the VM. The default is $true. .EXAMPLE PS> New-OSDeployHyperVM Creates, checkpoints, connects to, and starts a Generation 2 VM with the latest OSDeploy ISO when one is available. .EXAMPLE PS> New-OSDeployHyperVM -ISO 'D:\ISO\WinPE.iso' -StartVM $false Creates and checkpoints a VM with the specified ISO, leaving the VM stopped. .EXAMPLE PS> New-OSDeployHyperVM -WhatIf Returns a planned VM result with creation, startup, checkpoint, and VMConnect statuses set to $false. .INPUTS None. This function does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject. Returns VM identity, media, disk, switch, sizing, and action-status properties. A successful creation result also includes DisplayResolution. When ShouldProcess declines, the planned result omits that property and sets Created, Started, Checkpointed, and StartVMConnect to $false. .NOTES Author: David Segura Company: Recast Software Version: 1.0.0 Date: 2026-08-28 Requires Windows 11 25H2 or later, PowerShell 7.4 or later installed from MSI, curl.exe, Hyper-V and its PowerShell commands, a physical host, and Administrator rights. Hyper-V enabled-pending-reboot state stops the command. .LINK https://learn.microsoft.com/powershell/module/hyper-v/new-vm #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([pscustomobject])] param ( [Parameter()] [ValidateScript({ if ([string]::IsNullOrWhiteSpace($_)) { return $true } if (-not (Test-Path -Path $_ -PathType Leaf)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] ISO path was not found: $_" } if ([IO.Path]::GetExtension($_) -notmatch '^\.iso$') { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] ISO must point to an .iso file: $_" } return $true })] [string]$ISO, [Parameter()] [string]$NamePrefix = 'OSDeploy', [Parameter()] [ValidateSet('1','2')] [UInt16]$Generation = 2, [Parameter()] [ValidateRange(2, 64)] [UInt16]$MemoryStartupGB = 8, [Parameter()] [ValidateRange(1, 64)] [UInt16]$ProcessorCount = 2, [Parameter()] [ValidateRange(8, 512)] [UInt16]$VHDSizeGB = 64, [Parameter()] [ValidateSet('640x480','800x600','1024x768','1152x864','1280x720', '1280x768','1280x800','1280x960','1280x1024','1360x768','1366x768', '1400x1050','1440x900','1600x900','1680x1050','1920x1080','1920x1200', '2560x1440','2560x1600','3840x2160','3840x2400','4096x2160')] [string]$DisplayResolution = '1600x900', [Parameter()] [string]$SwitchName, [Parameter()] [ValidateSet('MicrosoftWindows', 'MicrosoftUEFICertificateAuthority', 'OpenSourceShieldedVM')] [string]$SecureBootTemplate = 'MicrosoftWindows', [Parameter()] [bool]$CheckpointVM = $true, [Parameter()] [bool]$StartVM = $true ) #================================================= # Stop before creating a VM when a required host capability is missing. if (-not (Test-IsWindows11)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Windows 11 is required." } if (-not (Test-IsWindows1125H2)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Windows 11 25H2 (build 26200) is required." } if (-not (Test-PwshVersionMin)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] PowerShell 7.4 or higher is required." } if (-not (Test-PwshPSHome)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] The MSI installation of PowerShell 7 is required." } if (-not (Test-CommandCurl)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] curl.exe is required but was not found in the current PATH. curl.exe ships with Windows 10 1803+." } if (-not (Test-IsAdministrator)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Administrator rights are required. Re-run PowerShell as Administrator and try again." } #================================================= # Block nested virtualization because this workflow requires a physical host. # Function Requirements if (Test-IsVM) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] New-OSDeployHyperVM cannot run inside a virtual machine." } #================================================= # Select the newest OSDeploy ISO when no ISO was supplied; otherwise create an empty DVD drive. if ([string]::IsNullOrWhiteSpace($ISO)) { $bootImageRoot = Join-Path -Path $env:ProgramData -ChildPath 'OSDeployCore\boot' $latestISO = Get-ChildItem -Path $bootImageRoot -Filter 'bootmedia.iso' -Recurse -ErrorAction SilentlyContinue | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 if ($latestISO) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Auto-selected ISO: $($latestISO.FullName)" $ISO = $latestISO.FullName } else { Write-Verbose "[$($MyInvocation.MyCommand.Name)] No bootmedia.iso found in $bootImageRoot; VM will be created with an empty DVD drive." } } $requiredCommands = @( 'New-VM', 'Get-VMHost', 'Add-VMDvdDrive', 'Set-VMFirmware', 'Set-VMVideo', 'Get-VMIntegrationService', 'Enable-VMIntegrationService', 'Set-VMProcessor', 'Set-VMMemory', 'Set-VM', 'Get-VM', 'Start-VM', 'Checkpoint-VM', 'Get-VMSwitch' ) foreach ($commandName in $requiredCommands) { if (-not (Get-Command -Name $commandName -ErrorAction SilentlyContinue)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Required Hyper-V command '$commandName' was not found. Ensure Hyper-V PowerShell tools are installed." } } if (-not (Test-HyperVEnabled)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Hyper-V is not enabled. Run Install-OSDeploySoftware -Name 'hyperv'" } if (Test-HyperVEnablePending) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Hyper-V is enabled but requires a reboot. Please restart your computer." } $availableSwitches = Get-VMSwitch -ErrorAction SilentlyContinue # Choose a default virtual switch only when the caller did not supply one. if (-not $SwitchName -and $availableSwitches) { $defaultSwitch = $availableSwitches | Where-Object { $_.Name -eq 'Default Switch' } | Select-Object -First 1 if ($defaultSwitch) { $SwitchName = $defaultSwitch.Name } else { $SwitchName = ($availableSwitches | Select-Object -First 1).Name } } $vmName = "$((Get-Date).ToString('yyMMdd-HHmmss')) $NamePrefix" $vmHost = Get-VMHost -ErrorAction Stop $vhdPath = Join-Path -Path $vmHost.VirtualHardDiskPath -ChildPath "$vmName.vhdx" $memoryStartupBytes = $MemoryStartupGB * 1GB $vhdSizeBytes = $VHDSizeGB * 1GB # Honor WhatIf and Confirm by returning the planned VM configuration without creating it. if (-not $PSCmdlet.ShouldProcess($vmName, 'Create and configure Hyper-V virtual machine')) { return [pscustomobject]@{ VMName = $vmName ISOPath = $ISO VHDPath = $vhdPath SwitchName = $SwitchName Generation = $Generation MemoryStartupGB= $MemoryStartupGB ProcessorCount = $ProcessorCount VHDSizeGB = $VHDSizeGB Created = $false Started = $false Checkpointed = $false StartVMConnect = $false } } # Attach the selected switch when available; otherwise create an unconnected VM. 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 } # Attach the supplied or discovered ISO; otherwise add an empty DVD drive. if ($ISO) { $dvdDrive = $vm | Add-VMDvdDrive -Path $ISO -Passthru -ErrorAction Stop } else { $dvdDrive = $vm | Add-VMDvdDrive -Passthru -ErrorAction Stop } if ($Generation -eq 2 -and $dvdDrive) { $vm | Set-VMFirmware -FirstBootDevice $dvdDrive -EnableSecureBoot On -SecureBootTemplate $SecureBootTemplate -ErrorAction Stop if (Get-Command -Name 'Get-TPM' -ErrorAction SilentlyContinue) { $tpm = Get-TPM if ($tpm.TpmPresent -eq $true -and $tpm.TpmReady -eq $true) { if (Get-Command -Name 'Set-VMSecurity' -ErrorAction SilentlyContinue) { $vm | Set-VMSecurity -VirtualizationBasedSecurityOptOut:$false -ErrorAction Stop } if (Get-Command -Name 'Set-VMKeyProtector' -ErrorAction SilentlyContinue) { $vm | Set-VMKeyProtector -NewLocalKeyProtector -ErrorAction Stop } if (Get-Command -Name 'Enable-VMTPM' -ErrorAction SilentlyContinue) { $vm | Enable-VMTPM -ErrorAction Stop } } } } $vm | Set-VMMemory -DynamicMemoryEnabled $false -ErrorAction Stop $vm | Set-VMProcessor -Count $ProcessorCount -ErrorAction Stop $horizontalResolution = [int]($DisplayResolution.Split('x')[0]) $verticalResolution = [int]($DisplayResolution.Split('x')[1]) $vm | Set-VMVideo -HorizontalResolution $horizontalResolution -VerticalResolution $verticalResolution -ResolutionType Single -ErrorAction Stop $integrationService = Get-VMIntegrationService -VMName $vm.Name -ErrorAction SilentlyContinue | 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' } if ($integrationService) { $vm | Get-VMIntegrationService -Name $integrationService.Name | Enable-VMIntegrationService -ErrorAction SilentlyContinue } $vm | Set-VM -AutomaticCheckpointsEnabled $false -AutomaticStartAction Nothing -AutomaticStartDelay 3 -AutomaticStopAction Shutdown -ErrorAction Stop $checkpointed = $false # Create the initial checkpoint only when requested. if ($CheckpointVM) { $vm | Checkpoint-VM -SnapshotName 'New-OSDeployHyperVM' -ErrorAction Stop $checkpointed = $true } $started = $false $startVmConnnect = $false # Start and optionally open VMConnect only when requested. if ($StartVM) { if (Get-Command -Name 'vmconnect.exe' -ErrorAction SilentlyContinue) { vmconnect.exe $env:ComputerName $vmName Start-Sleep -Seconds 10 $startVmConnnect = $true } $vm | Start-VM -ErrorAction Stop $started = $true } $finalVm = Get-VM -Name $vmName -ErrorAction Stop [pscustomobject]@{ VMName = $finalVm.Name ISOPath = if ($ISO) { $ISO } else { $null } VHDPath = $vhdPath SwitchName = $SwitchName Generation = $Generation MemoryStartupGB = $MemoryStartupGB ProcessorCount = $ProcessorCount VHDSizeGB = $VHDSizeGB DisplayResolution = $DisplayResolution Created = $true Started = $started Checkpointed = $checkpointed StartVMConnect = $startVmConnnect } } Register-ArgumentCompleter -CommandName New-OSDeployHyperVM -ParameterName 'SwitchName' -ScriptBlock { Get-VMSwitch | Select-Object -ExpandProperty Name | ForEach-Object { if ($_.Contains(' ')) { "'$_'" } else { $_ } } } |