Public/New-HyperVVM.ps1

function New-HyperVVM {
    <#
    .SYNOPSIS
        Creates one or more Hyper-V virtual machines.
 
    .DESCRIPTION
        Creates Hyper-V virtual machines with configurable OS disks (differencing or new),
        optional data disks, TPM, nested virtualization, and more. Supports remote execution
        via -ComputerName or -CimSession and bulk creation via pipeline input or arrays.
 
    .PARAMETER VMName
        Name(s) of the virtual machine(s) to create. Accepts an array of strings and
        pipeline input.
 
    .PARAMETER Path
        Path to the folder where VM files will be stored.
 
    .PARAMETER VMSwitch
        Name of the virtual switch to connect VMs to.
 
    .PARAMETER ParentDisk
        Path to a sysprepped parent disk for creating differencing OS disks.
        When omitted, a new dynamic disk is created.
 
    .PARAMETER VMGeneration
        Generation of the VM. Valid values are 1 and 2. Defaults to 2.
 
    .PARAMETER VMProcessorCount
        Number of virtual processors. Defaults to 2.
 
    .PARAMETER VMMemoryStartupBytes
        Startup memory in bytes. Defaults to 2GB.
 
    .PARAMETER OSDiskSizeBytes
        Size of the OS disk in bytes when no ParentDisk is specified. Defaults to 127GB.
 
    .PARAMETER DataDiskSizeBytes
        Size of the additional data disk in bytes. Defaults to 127GB.
 
    .PARAMETER AdditionalHDD
        When specified, creates a secondary data disk.
 
    .PARAMETER DisableNestedVirtualization
        When specified, nested virtualization is not enabled. Nested virtualization is enabled by default.
 
    .PARAMETER DisableTPM
        When specified, the virtual TPM is not enabled. TPM is enabled by default.
 
    .PARAMETER DisableGuestServices
        When specified, the Guest Service Interface integration service is not enabled.
        Guest Services are enabled by default.
 
    .PARAMETER PowerOnVM
        When specified, starts the VM after creation.
 
    .PARAMETER HorizontalResolution
        Horizontal video resolution. Must be an even number. Defaults to 1920.
 
    .PARAMETER VerticalResolution
        Vertical video resolution. Must be an even number. Defaults to 1080.
 
    .PARAMETER AutomaticStartAction
        Action when the Hyper-V host starts. Defaults to 'Nothing'.
 
    .PARAMETER AutomaticStopAction
        Action when the Hyper-V host shuts down. Defaults to 'ShutDown'.
 
    .PARAMETER ISOPath
        Path to an ISO file to attach for OS installation.
 
    .PARAMETER ComputerName
        Remote Hyper-V host(s) to create VMs on. Uses Invoke-Command for remote execution.
 
    .PARAMETER Credential
        Credential for authenticating to remote Hyper-V hosts.
 
    .PARAMETER CimSession
        Existing CIM session(s) to use for remote Hyper-V operations.
 
    .PARAMETER Configuration
        One or more HyperV.VMFactory.Configuration objects created by New-HyperVVMConfiguration.
        Accepts pipeline input.
 
    .EXAMPLE
        New-HyperVVM -VMName 'WebServer01' -Path 'D:\VMs' -VMSwitch 'External'
 
        Creates a single VM with default settings on the local host.
 
    .EXAMPLE
        'DC01', 'DC02', 'DC03' | New-HyperVVM -Path 'D:\VMs' -VMSwitch 'Internal'
 
        Creates three VMs via pipeline input with TPM and nested virtualization enabled (default).
 
    .EXAMPLE
        New-HyperVVM -VMName 'TestVM' -Path 'C:\VMs' -VMSwitch 'Default Switch' `
            -ParentDisk 'C:\Base\Win11.vhdx' -AdditionalHDD -PowerOnVM
 
        Creates a VM with a differencing disk from a parent image, adds a data disk,
        and starts the VM after creation.
 
    .EXAMPLE
        $configs = @(
            New-HyperVVMConfiguration -VMName 'App01' -Path 'D:\VMs' -VMSwitch 'External' -AdditionalHDD
            New-HyperVVMConfiguration -VMName 'App02' -Path 'D:\VMs' -VMSwitch 'External' -PowerOnVM
        )
        New-HyperVVM -Configuration $configs
 
        Creates multiple VMs with different configurations using the -Configuration parameter.
 
    .EXAMPLE
        New-HyperVVM -VMName 'RemoteVM' -Path 'D:\VMs' -VMSwitch 'External' `
            -ComputerName 'HyperVHost01' -Credential (Get-Credential)
 
        Creates a VM on a remote Hyper-V host.
 
    .EXAMPLE
        New-HyperVVM -VMName 'LabVM' -Path 'C:\VMs' -VMSwitch 'Internal' `
            -ISOPath 'C:\ISO\Ubuntu.iso' -VMGeneration 2 -DisableNestedVirtualization -DisableTPM
 
        Creates a VM configured to boot from an ISO with nested virtualization and TPM disabled.
 
    .OUTPUTS
        Microsoft.HyperV.PowerShell.VirtualMachine
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'ByParameter')]
    [OutputType([System.Object])]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline,
            ParameterSetName = 'ByParameter')]
        [ValidateNotNullOrEmpty()]
        [System.String[]]
        $VMName,

        [Parameter(Mandatory, ParameterSetName = 'ByParameter')]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Path,

        [Parameter(Mandatory, ParameterSetName = 'ByParameter')]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $VMSwitch,

        [Parameter(ParameterSetName = 'ByParameter')]
        [System.String]
        $ParentDisk,

        [Parameter(ParameterSetName = 'ByParameter')]
        [ValidateSet(1, 2)]
        [System.Int32]
        $VMGeneration = 2,

        [Parameter(ParameterSetName = 'ByParameter')]
        [ValidateRange(1, 128)]
        [System.Int32]
        $VMProcessorCount = 2,

        [Parameter(ParameterSetName = 'ByParameter')]
        [System.Int64]
        $VMMemoryStartupBytes = 2GB,

        [Parameter(ParameterSetName = 'ByParameter')]
        [System.Int64]
        $OSDiskSizeBytes = 127GB,

        [Parameter(ParameterSetName = 'ByParameter')]
        [System.Int64]
        $DataDiskSizeBytes = 127GB,

        [Parameter(ParameterSetName = 'ByParameter')]
        [System.Management.Automation.SwitchParameter]
        $AdditionalHDD,

        [Parameter(ParameterSetName = 'ByParameter')]
        [System.Management.Automation.SwitchParameter]
        $DisableNestedVirtualization,

        [Parameter(ParameterSetName = 'ByParameter')]
        [System.Management.Automation.SwitchParameter]
        $DisableTPM,

        [Parameter(ParameterSetName = 'ByParameter')]
        [System.Management.Automation.SwitchParameter]
        $DisableGuestServices,

        [Parameter(ParameterSetName = 'ByParameter')]
        [System.Management.Automation.SwitchParameter]
        $PowerOnVM,

        [Parameter(ParameterSetName = 'ByParameter')]
        [ValidateScript({
            if ($_ % 2) { throw 'HorizontalResolution must be an even number.' }
            $true
        })]
        [System.Int64]
        $HorizontalResolution = 1920,

        [Parameter(ParameterSetName = 'ByParameter')]
        [ValidateScript({
            if ($_ % 2) { throw 'VerticalResolution must be an even number.' }
            $true
        })]
        [System.Int64]
        $VerticalResolution = 1080,

        [Parameter(ParameterSetName = 'ByParameter')]
        [ValidateSet('Nothing', 'StartIfRunning', 'Start')]
        [System.String]
        $AutomaticStartAction = 'Nothing',

        [Parameter(ParameterSetName = 'ByParameter')]
        [ValidateSet('TurnOff', 'Save', 'ShutDown')]
        [System.String]
        $AutomaticStopAction = 'ShutDown',

        [Parameter(ParameterSetName = 'ByParameter')]
        [System.String]
        $ISOPath,

        [Parameter(ValueFromPipeline, ParameterSetName = 'ByConfiguration')]
        [PSTypeName('HyperV.VMFactory.Configuration')]
        [System.Object[]]
        $Configuration,

        [Parameter()]
        [System.String[]]
        $ComputerName,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $Credential,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimSession[]]
        $CimSession
    )

    begin {
        if (-not $ComputerName -and -not $CimSession) {
            Assert-HyperVPrerequisite
        }
    }

    process {
        $vmConfigs = @()

        if ($PSCmdlet.ParameterSetName -eq 'ByConfiguration') {
            $vmConfigs = @($Configuration)
        } else {
            foreach ($name in $VMName) {
                $vmConfigs += [PSCustomObject]@{
                    VMName                = $name
                    Path                  = $Path
                    VMSwitch              = $VMSwitch
                    ParentDisk            = $ParentDisk
                    VMGeneration          = $VMGeneration
                    VMProcessorCount      = $VMProcessorCount
                    VMMemoryStartupBytes  = $VMMemoryStartupBytes
                    OSDiskSizeBytes       = $OSDiskSizeBytes
                    DataDiskSizeBytes     = $DataDiskSizeBytes
                    AdditionalHDD         = [bool]$AdditionalHDD
                    NestedVirtualization  = -not $DisableNestedVirtualization
                    TPM                   = -not $DisableTPM
                    GuestServices         = -not $DisableGuestServices
                    PowerOnVM             = [bool]$PowerOnVM
                    HorizontalResolution  = $HorizontalResolution
                    VerticalResolution    = $VerticalResolution
                    AutomaticStartAction  = $AutomaticStartAction
                    AutomaticStopAction   = $AutomaticStopAction
                    ISOPath               = $ISOPath
                }
            }
        }

        foreach ($config in $vmConfigs) {
            $currentVMName = $config.VMName
            $target = if ($ComputerName) { $ComputerName -join ', ' } else { 'localhost' }

            if (-not $PSCmdlet.ShouldProcess("VM '$currentVMName' on $target", 'Create')) {
                continue
            }

            if ($ComputerName) {
                $result = Invoke-HyperVVMCreationRemote -Config $config -ComputerName $ComputerName `
                    -Credential $Credential
                if ($result) { $result }
                continue
            }

            try {
                Write-Verbose "Creating VM '$currentVMName'..."
                $vm = New-HyperVVMLocal -Config $config -CimSession $CimSession
                $vm
            } catch {
                Write-Error "Failed to create VM '$currentVMName': $_"
            }
        }
    }
}

function New-HyperVVMLocal {
    <#
    .SYNOPSIS
        Internal function that creates a VM on the local host or via CimSession.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Object]
        $Config,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimSession[]]
        $CimSession
    )

    $vmName = $Config.VMName
    $vmPath = $Config.Path
    $vhdFolder = Join-Path -Path $vmPath -ChildPath "$vmName\Virtual Hard Disks"
    $vmOSDisk = Join-Path -Path $vhdFolder -ChildPath "${vmName}_C.vhdx"

    # Build New-VM parameters
    $newVMSplat = @{
        Name       = $vmName
        Path       = $vmPath
        Generation = $Config.VMGeneration
        NoVHD      = $true
        MemoryStartupBytes = $Config.VMMemoryStartupBytes
    }
    if ($CimSession) {
        $newVMSplat['CimSession'] = $CimSession
    }

    Write-Verbose "Creating VM '$vmName' (Generation $($Config.VMGeneration))..."
    $vm = New-VM @newVMSplat

    # Connect network adapter
    Write-Verbose "Connecting VM '$vmName' to switch '$($Config.VMSwitch)'."
    $connectSplat = @{ SwitchName = $Config.VMSwitch; VMName = $vm.VMName }
    if ($CimSession) { $connectSplat['CimSession'] = $CimSession }
    Connect-VMNetworkAdapter @connectSplat

    # Create OS disk
    $osDiskSplat = @{ Path = $vmOSDisk }
    if ($Config.ParentDisk) {
        $osDiskSplat['ParentDisk'] = $Config.ParentDisk
    } else {
        $osDiskSplat['SizeBytes'] = $Config.OSDiskSizeBytes
    }
    $null = New-VMDisk @osDiskSplat

    # Attach OS disk
    Write-Verbose "Attaching OS disk to VM '$vmName'."
    Add-VMHardDiskDrive -VMName $vmName -ControllerType SCSI -Path $vmOSDisk

    # Handle boot device (ISO or HDD)
    if ($Config.ISOPath) {
        Write-Verbose "Attaching ISO '$($Config.ISOPath)' and setting as first boot device."
        Add-VMDvdDrive -VMName $vmName -Path $Config.ISOPath
        if ($Config.VMGeneration -eq 2) {
            $firstBootDevice = Get-VMDvdDrive -VMName $vmName
            if ($firstBootDevice) {
                Set-VMFirmware -VMName $vmName -FirstBootDevice $firstBootDevice
            }
        }
    } else {
        if ($Config.VMGeneration -eq 2) {
            $firstBootDevice = Get-VMHardDiskDrive -VMName $vmName
            if ($firstBootDevice) {
                Set-VMFirmware -VMName $vmName -FirstBootDevice $firstBootDevice
            }
        }
    }

    # Apply post-creation configuration
    $configSplat = @{
        VM                    = $vm
        ProcessorCount        = $Config.VMProcessorCount
        HorizontalResolution  = $Config.HorizontalResolution
        VerticalResolution    = $Config.VerticalResolution
        NestedVirtualization  = $Config.NestedVirtualization
        TPM                   = $Config.TPM
        GuestServices         = $Config.GuestServices
        AutomaticStartAction  = $Config.AutomaticStartAction
        AutomaticStopAction   = $Config.AutomaticStopAction
    }
    Set-VMConfiguration @configSplat

    # Create additional data disk
    if ($Config.AdditionalHDD) {
        $vmDataDisk = Join-Path -Path $vhdFolder -ChildPath "${vmName}_D.vhdx"
        Write-Verbose "Creating additional data disk for VM '$vmName'."
        $null = New-VMDisk -Path $vmDataDisk -SizeBytes $Config.DataDiskSizeBytes
        Add-VMHardDiskDrive -VMName $vmName -Path $vmDataDisk -ControllerType SCSI
    }

    # Power on VM
    if ($Config.PowerOnVM) {
        Write-Verbose "Starting VM '$vmName'."
        Start-VM -VMName $vmName
    }

    return $vm
}

function Invoke-HyperVVMCreationRemote {
    <#
    .SYNOPSIS
        Internal function that creates a VM on a remote Hyper-V host via Invoke-Command.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Object]
        $Config,

        [Parameter(Mandatory)]
        [System.String[]]
        $ComputerName,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    # Convert config to hashtable for serialization
    $configHash = @{
        VMName                = $Config.VMName
        Path                  = $Config.Path
        VMSwitch              = $Config.VMSwitch
        ParentDisk            = $Config.ParentDisk
        VMGeneration          = $Config.VMGeneration
        VMProcessorCount      = $Config.VMProcessorCount
        VMMemoryStartupBytes  = $Config.VMMemoryStartupBytes
        OSDiskSizeBytes       = $Config.OSDiskSizeBytes
        DataDiskSizeBytes     = $Config.DataDiskSizeBytes
        AdditionalHDD         = $Config.AdditionalHDD
        NestedVirtualization  = $Config.NestedVirtualization
        TPM                   = $Config.TPM
        GuestServices         = $Config.GuestServices
        PowerOnVM             = $Config.PowerOnVM
        HorizontalResolution  = $Config.HorizontalResolution
        VerticalResolution    = $Config.VerticalResolution
        AutomaticStartAction  = $Config.AutomaticStartAction
        AutomaticStopAction   = $Config.AutomaticStopAction
        ISOPath               = $Config.ISOPath
    }

    $scriptBlock = {
        param($VMConfig)

        $vmName = $VMConfig.VMName
        $vmPath = $VMConfig.Path
        $vhdFolder = Join-Path -Path $vmPath -ChildPath "$vmName\Virtual Hard Disks"
        $vmOSDisk = Join-Path -Path $vhdFolder -ChildPath "${vmName}_C.vhdx"

        # Create VM
        $newVMSplat = @{
            Name               = $vmName
            Path               = $vmPath
            Generation         = $VMConfig.VMGeneration
            NoVHD              = $true
            MemoryStartupBytes = $VMConfig.VMMemoryStartupBytes
        }
        $vm = New-VM @newVMSplat

        # Connect network
        Connect-VMNetworkAdapter -SwitchName $VMConfig.VMSwitch -VMName $vm.VMName

        # Create OS disk
        if ($VMConfig.ParentDisk) {
            New-VHD -Path $vmOSDisk -ParentPath $VMConfig.ParentDisk -Differencing
        } else {
            New-VHD -Path $vmOSDisk -Dynamic -SizeBytes $VMConfig.OSDiskSizeBytes
        }

        Add-VMHardDiskDrive -VM $vm -ControllerType SCSI -Path $vmOSDisk

        # Boot device
        if ($VMConfig.ISOPath) {
            Add-VMDvdDrive -VM $vm -Path $VMConfig.ISOPath
            if ($VMConfig.VMGeneration -eq 2) {
                Set-VMFirmware -VM $vm -FirstBootDevice (Get-VMDvdDrive -VM $vm)
            }
        } else {
            if ($VMConfig.VMGeneration -eq 2) {
                Set-VMFirmware -VM $vm -FirstBootDevice (Get-VMHardDiskDrive -VM $vm)
            }
        }

        # Configure VM
        Set-VM -VM $vm -ProcessorCount $VMConfig.VMProcessorCount `
            -AutomaticCheckpointsEnabled:$false `
            -AutomaticStartAction $VMConfig.AutomaticStartAction `
            -AutomaticStopAction $VMConfig.AutomaticStopAction

        Set-VMVideo -VM $vm -ResolutionType Single `
            -HorizontalResolution $VMConfig.HorizontalResolution `
            -VerticalResolution $VMConfig.VerticalResolution

        if ($VMConfig.GuestServices) {
            Enable-VMIntegrationService -VM $vm -Name 'Guest Service Interface'
        }

        if ($VMConfig.NestedVirtualization) {
            Set-VMProcessor -VM $vm -ExposeVirtualizationExtensions:$true
        }

        if ($VMConfig.TPM) {
            Set-VMKeyProtector -VM $vm -NewLocalKeyProtector
            Enable-VMTPM -VM $vm
        }

        # Additional data disk
        if ($VMConfig.AdditionalHDD) {
            $vmDataDisk = Join-Path -Path $vhdFolder -ChildPath "${vmName}_D.vhdx"
            New-VHD -Path $vmDataDisk -Dynamic -SizeBytes $VMConfig.DataDiskSizeBytes
            Add-VMHardDiskDrive -VM $vm -Path $vmDataDisk -ControllerType SCSI
        }

        # Power on
        if ($VMConfig.PowerOnVM) {
            Start-VM -VM $vm
        }

        return $vm
    }

    $invokeParams = @{
        ComputerName = $ComputerName
        ScriptBlock  = $scriptBlock
        ArgumentList = @(, $configHash)
    }
    if ($Credential) {
        $invokeParams['Credential'] = $Credential
    }

    Write-Verbose "Creating VM '$($Config.VMName)' on remote host(s): $($ComputerName -join ', ')"
    Invoke-Command @invokeParams
}