Update 11/25/2021: There’s anther variation of this function at: https://blog.kimconnect.com/powershell-create-hyper-v-guest-vm-from-virtual-disk-vhdx/

This function will dynamically detect source disks and clone each one. The down side to this method is that accessing the source cluster is a requirement to enable querying source VM objects.

# cloneVm.ps1
# Version 0.02

# Change these variables
$sourceVmName='WindowsGoldenImage'
$newVmNames=@(
    'SERVER001', 
    'SERVER002',
    'SERVER003'
)
$destinationFolder='C:\ClusterStorage\Volume5'
$memory='8GB'
$cpus=4
$network='IntranetZone'
$vlan='100'
$secureBoot=$false # Linux machines require this to be false - Windows can work with value of True
$extraDiskSize='200GB' # set this value to $Null if D: volume is not required

# Assuming single Vhdx file VM cloning
function createVmFromVhdx{
    param(
        $sourceVhdx,
        $newVmName,
        $destinationFolder,
        $memory='4GB',
        $cpus=2,
        $network,
        $vlan,
        $extraDiskSize=$null,
        $secureBoot=$false,
        $addVmToCluster=$true,
        $onlineVm=$true
    )
    $ErrorActionPreference = 'stop'
    function addVmToCluster{
        param($vmNames,$targetCluster)
        $results=@()
        foreach ($vmName in $vmNames){
            try{
                #Start-VM -Name $vmName -EA Stop
                if(!$targetCluster){$targetCluster=(get-cluster -ea SilentlyContinue).Name}
                if($targetCluster){
                    Add-ClusterVirtualMachineRole -Cluster $targetCluster -VirtualMachine $vmName -EA Stop
                    $results+=[hashtable]@{$vmName=$true}
                }else{
                    write-host "No clusters defined."
                    $results+=[hashtable]@{$vmName=$false}
                    }
            }catch{
                write-warning "$($error[0])"
                $results+=[hashtable]@{$vmName=$false}
                }
        }
        return $results
    }

    try{
        $newFolder="$destinationFolder\$newVmName" 
        if(!(test-path $newFolder)){new-item -ItemType Directory -Path $newFolder -force}              
        $newVhdx="$newFolder\$newVmName`_disk0.vhdx"
        if(!(test-path $newVhdx)){
            New-Item -ItemType File -Path $newVhdx -Force # touch before copying contents
            Copy-Item -Path $sourceVhdx -Destination $newVhdx
        }else{
            write-warning "Volume $newVhdx already exists. Thus, that VMDK will be used instead of a clone."
            }
        New-VM -Name $newVmName `
                -MemoryStartupBytes $($memory/1) `
                -BootDevice VHD `
                -VHDPath $newVhdx `
                -Path $newFolder `
                -Generation 2 `
                -Switch $network         
        if($vlan){Set-VMNetworkAdapterVlan -VMName $newVmName -Access -VlanId $vlan}
                if($cpus -gt 1){Set-VMProcessor $newVmName -Count $cpus}
        Set-VMProcessor $newVmName -CompatibilityForMigrationEnabled $true
        if(!$secureBoot){Set-VMFirmware -VMName $newVmName -DisableSecureBoot}
        # Adding disks (optional)
        if($extraDiskSize){        
            $diskFile=(join-path $destinationFolder $newVmName) + "\$newVmName`_disk1.vhdx"
            NEW-VHD -Fixed $diskFile -SizeBytes (invoke-expression $extraDiskSize) -ea Stop
            Add-VMHardDiskDrive -VMName $newVmName -Path $diskFile            
        }
        if($addVmToCluster){
            addVmToCluster $newVmname
            $moved=if(get-cluster -ea SilentlyContinue){get-vm $newVmName|Move-ClusterVirtualMachineRole}else{$false}
            if($moved){write-host "'$newVmname' has been moved to $($moved.OwnerNode)"}
        }
        if($onlineVm){
            start-vm $newVmName
        }
        return $true
    }catch{
        write-warning "$($error[0])"
        return $false
    }
}

foreach($newVmName in $newVMNames){
    createVmFromVhdx $sourceVhdx `
        $newVmName `
        $destinationFolder `
        $memory `
        $cpus `
        $network `
        $vlan `
        $extraDiskSize `
        $secureBoot `
        $addVmToCluster `
        $onlineVm
}

#####################
# Join domain
#
#####################

Add-Computer -DomainName 'intranet.kimconnect.com' `
    -credential kimconnect.com\DomainAdmin `
    -ComputerName $env:computernname `
    -newname 'NEWCOMPUTERNAME' -restart
# cloneVm.ps1
# Version 0.01

# Change these variables
$sourceVmName='WindowsGoldenImage'
$newVmName='NewServerName'
$destinationFolder='C:\ClusterStorage\Volume5'
$memory='8GB'
$cpus=4
$network='IntranetZone'
$vlan='100'
$secureBoot=$false # Linux machines require this to be false - Windows can work with value of True

function cloneVM{
    param(
        $sourceVmName,
        $newVmName,
        $destinationFolder,
        $memory='4GB',
$cpus=2,
        $network,
        $vlan,
$secureboot=$true
    )
    $ErrorActionPreference = 'stop'       
    
    function confirmation($content,$testValue="I confirm",$maxAttempts=3){
        $confirmed=$false;
        $attempts=0;        
        $content|write-host
        write-host "Please review this content for accuracy.`r`n"
        while ($attempts -le $maxAttempts){
            if($attempts++ -ge $maxAttempts){
                write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
                break;
                }
            $userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm";
            if ($userInput.ToLower() -ne $testValue.ToLower()){
                cls;
                $content|write-host
                write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again..`r`n"
                }else{
                    $confirmed=$true;
                    write-host "Confirmed!`r`n";
                    break;
                    }
            }
        return $confirmed;
    }
    
    try{
        $newFolder="$destinationFolder\$newVmName"  
        if(!(test-path $newFolder)){
            $null=new-item -ItemType Directory -Path $newFolder -force
            write-host "Created New Folder: $newFolder"
            }
        $sourceDisks=(get-vhd (Get-VM $sourceVmName).VMid).Path        
        $newVhdxPaths=@{} 
        for ($i=0;$i -lt $sourceDisks.count;$i++){
            $diskNumber='disk'+$i
            if($sourceDisks.count -gt 1){
                $sourceDisk=$sourceDisks[$i]
                }
            else{
                $sourceDisk=$sourceDisks
                }
            $newVhdx="$newFolder\$newVmName`_$diskNumber.vhdx"
            if(!(test-path $newVhdx)){                
                $null=New-Item -ItemType File -Path $newVhdx -Force # touch before copying contents
                write-host "Copy-Item -Path $sourceDisk -Destination $newVhdx"
Copy-Item -Path $sourceDisk -Destination $newVhdx
                }        
            else{
                $confirm=confirmation "Destroy existing contents of $newVhdx ?"
                if($confirm){
                    Copy-Item -Path $sourceDisk -Destination $newVhdx
                    write-host "Overwritten: $newVhdx" -ForegroundColor Yellow
                    }
                else{
                    write-warning "Re-using the existing contents of: $newVhdx"
                    }
                }
            $newVhdxPaths[$diskNumber]=$newVhdx
            }
        write-host ($newVhdxPaths.GetEnumerator() | % { "$($_.Key)=$($_.Value)" })
        pause
        $createVmCommand="
                New-VM -Name $newVmName ``
                -MemoryStartupBytes $($memory/1) ``
                -BootDevice VHD ``
                -VHDPath $($newVhdxPaths['disk0']) ``
                -Path $newFolder ``
                -Generation 2 ``
                -Switch $network"
        write-host $createVmCommand
        pause
        Invoke-Expression $createVmCommand
        
        if ($newVhdxPaths.Count -gt 1){
            for ($j=1;$j -lt $newVhdxPaths.count;$j++){
                $diskPath=$newVhdxPaths[$('disk'+$j)]
                write-host "Attaching $diskPath..."
                Add-VMHardDiskDrive -VMName $newVmName -path $newVhdxPaths[$newDiskNumber]
                }       
            }

        if($vlan){Set-VMNetworkAdapterVlan -VMName $newVmName -Access -VlanId $vlan}
        if($cpus -gt 1){Set-VMProcessor $newVmName -Count $cpus}
        Set-VMProcessor $newVmName -CompatibilityForMigrationEnabled $true
        if(!$secureBoot){Set-VMFirmware -VMName $newVmName -DisableSecureBoot}

Start-VM -Name $newVmName
        VMConnect.exe $env:computername $newVmName
        }
    catch{
        write-warning "$($error[0])"
        }
}

cloneVM $sourceVmName $newVmName $destinationFolder $memory $cpus $network $vlan $secureBoot
# Troubleshooting:
# Error occurs when 'Path' was not specified during VM Creation, then such VM was being added to a cluster
#The path where the virtual machine hard disk is stored, TESTWINDOWS.vhdx, is not on storage managed by this failover cluster. Verify that the path is #accessible from all nodes of the cluster that can own this virtual machine.
#
#Disk path 'C:\ProgramData\Microsoft\Windows\Hyper-V' is not a path to storage in the cluster or to storage that can be added to the cluster. You must ensure this storage is available to every node in the cluster to make this virtual machine highly available.

This next function is convenient to generate new clone machines from a single VHDX file. The advantage of this function over the previous is that it only requires read-only access to the source VHDX file at the cost of not flexible enough to access multiple-disk source machines.

# Change these variables
$sourceVhdx='\\Intranet\WindowsGoldenImage\WindowsGoldenImage_disk0.vhdx'
$newVmName='NewServerName'
$destinationFolder='C:\ClusterStorage\Volume5'
$memory='8GB'
$cpus=4,
$network='IntranetZone'
$vlan='100'
$secureBoot=$true # Windows: True, Linux: False

# Assuming single Vhdx file VM cloning
function createVmFromVhdx{
    param(
        $sourceVhdx,
        $newVmName,
        $destinationFolder,
        $memory='4GB',
$cpus=2,
        $network,
        $vlan
    )
    $ErrorActionPreference = 'stop'
    try{
        $newFolder="$destinationFolder\$newVmName"  
        if(!(test-path $newFolder)){new-item -ItemType Directory -Path $newFolder -force}              
        $newVhdx="$newFolder\$newVmName`_disk0.vhdx"
        if(!(test-path $newVhdx)){
            New-Item -ItemType File -Path $newVhdx -Force # touch before copying contents
            Copy-Item -Path $sourceVhdx -Destination $newVhdx
            }        
        else{
            write-warning "Volume $newVhdx already exists. Thus, that VMDK will be used instead of a clone."
            }
        New-VM -Name $newVmName `
                -MemoryStartupBytes $($memory/1) `
                -BootDevice VHD `
                -VHDPath $newVhdx `
-Path $newFolder `
                -Generation 2 `
                -Switch $network   
        
        if($vlan){Set-VMNetworkAdapterVlan -VMName $newVmName -Access -VlanId $vlan}
                if($cpus -gt 1){Set-VMProcessor $newVmName -Count $cpus}
        Set-VMProcessor $newVmName -CompatibilityForMigrationEnabled $true
        if(!$secureBoot){Set-VMFirmware -VMName $newVmName -DisableSecureBoot}
Start-VM -Name $newVmName
        VMConnect.exe $env:computername $newVmName
        }
    catch{
        write-warning "$($error[0])"
        }
}

createVmFromVhdx $sourceVhdx $newVmName $destinationFolder $memory $cpus $network $vlan $secureBoot

Optionally, guest VMs should be movable within a cluster of hosts. Thus, the following helper function makes that easy

$newVmName='New-Server-Name-Here'
$targetCluster='CLUSTER007'

function addVmToCluster{
    param($vmName,$targetCluster)
    try{
        if(!$targetCluster){
            $targetCluster=(get-cluster -ea SilentlyContinue).Name
            }
        if($targetCluster){
            Add-ClusterVirtualMachineRole -Cluster $targetCluster -VirtualMachine $vmName -EA Stop
            return $true
            }
        else{
            write-host "No clusters defined."
            return $false
            }
        }
    catch{
        write-warning "$($error[0])"
        return $false
        }
    }

addVmToCluster $newVmName $targetCluster