PowerShell: Assign Guest Virtual Machine to a Cloud in VMM

# assignVmsToCloud.ps1
# The following function assigns a guest VM into a 'cloud' in Virtual Machine Manager

$vmNames=@(
	'guest-vm1',
	'guest-vm2',
	'guest-vm3'
)
$cloudAssign='DEV'
$vmmServer=$env:computername
$roleName='Developers'

function assignVmsToCloud{
	param(
		$vmName='testVm',
		$cloudAssign='Test',
		$roleName='Administrators',
        $vmmServer='localhost'
	)
	$ErrorActionPreference='Stop'
	try{
        $null=import-module virtualmachinemanager
		$vm=Get-SCVirtualMachine -Name $vmName -VMMServer $vmmServer
		$cloud=Get-SCCloud -Name $cloudAssign -VMMServer $vmmServer
		$userRole=Get-SCUserRole -Name $roleName -VMMServer $vmmServer
		$jobGroup=[System.Guid]::NewGuid().Guid
        if(!$vm){
            write-warning "Unable to find VM named $vmName on VMM Server $vmmServer"
            return $false
        }elseif($vm.Cloud.Name -eq $cloudAssign){
			write-host "$vmName has already been assigned to Cloud $cloudAssign" -ForegroundColor Green
			return $true
		}else{	
			$null=Set-SCVirtualMachine -VM $vm -Cloud $cloud
			$null=Set-SCUserRoleQuota -Cloud $cloud -JobGroup $jobGroup
			$null=Set-SCUserRoleQuota -Cloud $cloud -JobGroup $jobGroup -QuotaPerUser
			$null=Add-SCUserRolePermission -Cloud $cloud -JobGroup $jobGroup -AllowLocalAdmin -Checkpoint -CheckpointRestoreOnly -DeployFromTemplateOnly -Deploy -DeployShielded -PauseAndResume -RemoteConnect -Remove -Save -Shutdown -Start -Stop -Store -ManageAzureProfiles
			$null=Grant-SCResource -Resource $vm -JobGroup $jobGroup
			$null=Set-SCUserRole -UserRole $userRole -JobGroup $jobGroup
			$vm=Get-SCVirtualMachine -VMMServer $vmmServer -Name $vmName
			if($vm.Cloud.Name -eq $cloudAssign){
				write-host "$vmName has been assigned to Cloud $cloudAssign successfully" -ForegroundColor Green
				return $true
			}else{
				write-host "$vmName has NOT been assigned to Cloud $cloudAssign" -ForegroundColor Red
				return $false
			}
		}
	}catch{
		write-warning $_
		return $false
	}
}

######## Set VM as defined ########
$results=[hashtable]@{}
foreach($vmname in $vmNames){
    $result=assignVmsToCloud $vmName $cloudAssign $roleName $vmmServer
	$results+=@{$vmName=$result}
}
######## Dynamically Gather VM by Keywords and assign to cloud ########
$keyword='stg'
$cloudAssign='STAGE'
$vms=.{$null=import-module virtualmachinemanager;get-vm}
$targetVms=$vms.Name|?{$_ -like "*$keyword*"}

$results=[hashtable]@{}
foreach($vmName in $targetVms){
    $result=assignVmsToCloud $vmName $cloudAssign $roleName $vmmServer
	$results+=@{$vmName=$result}
}

The Process of Adding a New Hyper-V Server Into a Cluster and the Associated Virtual Machine Manager (VMM)

These are the steps:

  1. Install Windows
    Windows 2019 Data Center Edition is the standard as of May 20, 2022
    Certain hardware may require slipstreamed ISO’s to load RAID drivers so that the installation wizard would recognize the underlying hardware
    Install Windows with GUI. Although, headless Windows would still work, our Admins prefer to have GUI, boo
  2. Setup Network connectivity
    Obtain a static IP for the server
    Assign an IP for new server using
    Setup Interface with static IP, Gateway, and DNS
  3. Ensure that machine is accessible to VMM on these ports
    Ports required by SCVMM:
    TCP/22
    TCP/80
    TCP/135
    TCP/139
    TCP/445
    TCP/443
    TCP/623
    TCP/1433
    TCP/5985
    TCP/5986
    TCP/8530
    TCP/8531
    TCP/8100
    TCP/8101
    TCP/8102
    TCP/49152 to TCP/65535
    Ports required by Prometheus Windows Exporter:
    TCP/9182
  4. Enable RDP
    This feature would automatically be installed when a machine joins the domain. However, we’re ensuring that the hardware and OS would not go into a recovery loop due to driver issues. Hence, we would be patching Windows and attempting enable its Hyper-V features as a precursor to joining the machine to to domain, only if the machine is stable after these functions are added.
  5. Disable Startup repair
    Run this command: bcdedit /set {current} recoveryenabled no
  6. Update Windows
    There’s a PowerShell method to updating Windows directly from Microsoft
    Alternative, the equivalent GUI method would suffice
  7. Install Hyper-V Features: Install-WindowsFeature -Name Hyper-V -IncludeManagementTools -Restart
    Be advised that Windows will reboot after this command
    If there were any driver conflicts, Windows would go into a recovery loop. Hopefully, that doesn’t happen to the machine you’re preparing
  8. Join Domain
  9. Run these PowerShell Command as Administrator
    # Install Chocolatey
    if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))}

    # Defining $ENV:ChocotaleyInstall so that it would be called by refreshenv
    $ENV:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
    Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
    Update-SessionEnvironment
    # Add Private Repo
    $privateRepoName='kimconnectrepo'
    $privateRepoUrl='https://choco.kimconnect.com/chocolatey'
    $priority=1
    $privateRepo="'$privateRepoName' -s '$privateRepoUrl' --priority=$priority"
    choco source add -n=$privateRepo

  10. Uninstall Windows Defender
    Run this command: Remove-WindowsFeature Windows-Defender
  11. Install Enterprise Antivirus
  12. Install Prometheus Windows Exporter
    Use Chocolatey to install Prometheus Windows_Exporter
    choco install prometheus-windows-exporter.install -y --ignore-checksums
    $null=& sc.exe failure $serviceName reset= 30 actions= restart/100000/restart/100000/""/300000
    Set-Service -Name $serviceName -StartupType 'Automatic'
    Enabling Collectors for a Hyper-V Server
    $serviceName='Windows_Exporter'
    $enabledCollectors='os,cpu,cs,logical_disk,net,tcp,hyperv,service,textfile'
    $switches=" --log.format logger:eventlog?name=$serviceName --collectors.enabled $enabledCollectors"
    function modifyService{
    param(
    $serviceName,
    $switches
    )
    $wmiService=Get-WmiObject win32_service| ?{$_.Name -like "*$serviceName*"}
    $exePath=[regex]::match($wmiService.PathName,'\"(.*)\"').groups[1]
    $binarySwitches='\"' + $exePath + '\"' + $switches
    sc.exe config $serviceName binpath= $binarySwitches
    sc.exe qc windows_exporter
    restart-service $serviceName
    }
    # Local execution
    modifyService $serviceName $switches
  13. Install Failover Clustering Features
    # After restart, install Failover clustering features
    Install-WindowsFeature Failover-Clustering -IncludeManagementTools; Install-WindowsFeature RSAT-Clustering-MGMT,RSAT-Clustering-PowerShell,Multipath-IO
  14. Setup NIC Teaming (if appropriate)
    # Check Teaming setup
    Get-NetLbfoTeam

    # Initialize Teaming, if required
    $teamName='Team1'
    $vlanId=100
    Add-NetLbfoTeamNIC -Team $teamName -VlanID $vlanId

    # Set Teaming mode, if required
    $teamName='Team1'
    $teamMode='LACP'
    $lbAlgorithm='Dynamic'
    Set-NetLbfoTeam -Name $teamName -TeamingMode $teamMode -LoadBalancingAlgorithm $lbAlgorithm

    # Add Team members, if required
    $teamName='Team1'
    $nicTeamMembers='NIC1','NIC2'
    $nicTeamMembers|%{Add-NetLbfoTeamMember -Name $_ -Team $teamName}

    # Check Teaming setup, if required
    Get-NetLbfoTeam

    # Check NIC names
    Get-NetAdapter

  15. Enable WinRM: Enable-PSRemoting -Force
  16. Enable CredSSP: Enable-WSManCredSSP -Role Server -Force
  17. Create New Virtual Switch(es)
    $switchName='External-Connection' # change this label to reflect the actual vSwitch name to be added
    $adapterName='TEAM1' # change this value to reflect the correct interface
    $vlanId=101 # change this to the correct VLAN
    function addVirtualSwitch($switchName,$adapterName,$vlanId){
    New-VMSwitch -name $switchName -NetAdapterName $adapterName -AllowManagementOS $true
    if($vlanId){
    Get-VMNetworkAdapter -SwitchName $switchName -ManagementOS|Set-VMNetworkAdapterVlan -Access -VlanId $vlanId
    }
    }
    addVirtualSwitch $switchName $adapterName $vlanId
  18. Include appropriate Admins
    # Example for HQ
    $admins=@(
    "$env:USERDOMAIN\$($env:computername)$",
    "$env:USERDOMAIN\service-hv",
    "$env:USERDOMAIN\service-vmm"
    )
    Add-localgroupmember -group Administrators -member $admins
  19. Join Node to Cluster
    # Example
    $clusterName='hyperv-cluster'
    Add-ClusterNode -Name $env:computername -Cluster $clusterName
  20. Install VMM Agent
    The easy method:
    # Install VMM Agent
    $vmmServer='hq-vmm01' # change this value to reflect the correct VMM node
    $version='10.19.2591.0' # change to value to reflect the latest expected version
    $agentMsiFile="\\$vmmServer\C$\Program Files\Microsoft System Center\Virtual Machine Manager\agents\amd64\$version\vmmAgent.msi"

    # Installing VMM Agent using its MSI File
    $file=gi $agentMsiFile
    $DataStamp = get-date -Format yyyyMMddTHHmmss
    $logFile ="C:\" + '{0}-{1}.log' -f $file.name,$DataStamp
    $MSIArguments = @(
    "/i"
    ('"{0}"' -f $file.fullname)
    "/qn"
    "/norestart"
    "/L*v"
    $logFile
    )
    Start-Process "msiexec.exe" -ArgumentList $MSIArguments -Wait -NoNewWindow
  21. Add This New Hyper-V Node to VMM
    Access VMM as Administrator > Navigate to Fabric > select the appropriate cluster > right-click the newly introduced node > Associate with Cluster > Done

Hyper-V Guest VM CPU Compatibility Feature Down Side

Symptom: guest VM could not install applications that require’SSE4.2 and POPCNT instruction sets’

Cause:

The compatibility feature has been turned on for this guest VM; thus, these CPU features have been disabled:

  • AMD: SSSE3, SSE4.1, SSE4.A, SSE5, POPCNT, LZCNT, Misaligned SSE, AMD 3DNow!, Extended AMD 3DNow!
  • Intel: SSSE3, SSE4.1, SSE4.2, POPCNT, Misaligned SSE, XSAVE, AVX

Resolution:

Disable (uncheck) the ‘allow migration to a virtual machine host with a different processor version’

How To Install VMM Agent (SCVMM) Manually

The following snippet assumes that a New Hyper-V Server has been added to the cluster; yet, it’s SCVMMAgent Service doesn’t get installed or is corrupted such as:

PS C:\Windows\system32> get-service SCVMMAgent
get-service : Cannot find any service with service name 'SCVMMAgent'.
At line:1 char:1
+ get-service SCVMMAgent
+ ~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (SCVMMAgent:String) [Get-Service], ServiceCommandException
    + FullyQualifiedErrorId : NoServiceFoundForGivenName,Microsoft.PowerShell.Commands.GetServiceCommand
# Version 0.01
# No robot crawler to locate the highest VMMAgent version automatically (will code that when I feel like it)

# Install VMM Agent
$vmmServer='vmmServerName'
$version='10.19.2591.0'
$agentMsiFile="\\$vmmServer\C$\Program Files\Microsoft System Center\Virtual Machine Manager\agents\amd64\$version\vmmAgent.msi"

# Installing VMM Agent using its MSI File
$file=gi $agentMsiFile
$DataStamp = get-date -Format yyyyMMddTHHmmss
$logFile ="C:\" + '{0}-{1}.log' -f $file.name,$DataStamp
$MSIArguments = @(
    "/i"
    ('"{0}"' -f $file.fullname)
    "/qn"
    "/norestart"
    "/L*v"
    $logFile
)
Start-Process "msiexec.exe" -ArgumentList $MSIArguments -Wait -NoNewWindow

PowerShell: Add Network Sites (VLAN) into a Virtual Machine Manager Logical Network

# addVmmNetworkSites.ps1
# version 0.01

# User Defined Variables
$networkSites=@(
    @{
        'newNetworkSitename'="test 1"
        'vlanId'=100
        'vlanCidr'='192.168.1.0/24'
        'logicalNetworkName'='Trunk'
        'logicalNetworkDescription'=''
        'hostGroup'='All Hosts'
    }
    @{
        'newNetworkSitename'="test 2"
        'vlanId'=200
        'vlanCidr'='192.168.2.0/24'
        'logicalNetworkName'='Trunk'
        'logicalNetworkDescription'=''
        'hostGroup'='All Hosts'
    }    
)

function addVmmNetworkSite{
    param(
        $newNetworkSitename,
        $vlanId,
        $vlanCidr,
        $logicalNetworkName='Trunk',
        $logicalNetworkDescription='',
        $hostGroup='All Hosts',        
        $runAsynchronously=$true
    )

    # Set the logical network with required features of network virtualization
    $logicalNetwork = Get-SCLogicalNetwork -Name $logicalNetworkName
    $setLogicalNetwork="Set-SCLogicalNetwork -Name '$logicalNetworkName' ``
        -Description '$logicalNetworkDescription' ``
        -LogicalNetwork `$logicalNetwork ``
        -EnableNetworkVirtualization `$true ``
        -UseGRE `$true ``
        -LogicalNetworkDefinitionIsolation `$false ``
        $(if($runAsynchronously){'-RunAsynchronously'})"
    write-host $setLogicalNetwork
    invoke-expression $setLogicalNetwork

    # Add network site
    $allHostGroups = @()
    $allHostGroups += Get-SCVMHostGroup -Name $hostGroup
    $allSubnetVlan = @()
    $allSubnetVlan += New-SCSubnetVLan -Subnet $vlanCidr -VLanID $vlanId
    $addNetworkSite="New-SCLogicalNetworkDefinition -Name '$newNetworkSitename' ``
        -LogicalNetwork `$logicalNetwork ``
        -VMHostGroup `$allHostGroups ``
        -SubnetVLan `$allSubnetVlan ``
        $(if($runAsynchronously){'-RunAsynchronously'})"
    write-host $addNetworkSite
    invoke-expression $addNetworkSite
}

foreach($networkSite in $networkSites){
    $command="addVmmNetworkSite -newNetworkSitename '$($networkSite.newNetworkSitename)' ``
        -vlanId '$($networkSite.vlanId)' ``
        -vlanCidr '$($networkSite.vlanCidr)' ``
        -logicalNetworkName '$($networkSite.logicalNetworkName)' ``
        -logicalNetworkDescription '$($networkSite.logicalNetworkDescription)' ``
        -hostGroup '$($networkSite.hostGroup)'"
    write-host $command
    #invoke-expression $command
}

Virtual Machine Networking Error 15011

Creating New Logical Network

$logicalNetworkID="somehash-hash-hash"
$newNetworkName='Test Network'
$subnet="192.168.500.0/24"
$logicalNetwork = Get-SCLogicalNetwork -ID $logicalNetworkID

$vmNetwork = New-SCVMNetwork -Name $newNetworkName -LogicalNetwork $logicalNetwork -IsolationType "WindowsNetworkVirtualization" -CAIPAddressPoolType "IPV4" -PAIPAddressPoolType "IPV4"
Write-Output $vmNetwork
$subnet = New-SCSubnetVLan -Subnet $subnet
New-SCVMSubnet -Name $newNetworkName -VMNetwork $vmNetwork -SubnetVLan $subnet -EnableEncryption $false

Remove Logical Network in VMM

$scvmNetworkName='Test Network'
Get-SCVMNetwork -Name $scvmNetworkName |Remove-SCVMNetwork

Error Message

Remove-SCVMNetwork : VMM is unable to delete the VMNetwork 'Test Network' because other objects, such as VMSubnets, Load balancer templates and  Virtual network adapters depend on it. (Error ID: 15011)

Remove the VMNetwork association with all dependent resources, and then remove the VMNetwork.

To restart the job, run the following command:
PS> Restart-Job -Job (Get-VMMServer localhost | Get-Job | where { $_.ID -eq "{somehash-hash-hash}"})
At line:1 char:47
+ Get-SCVMNetwork -Name "Test Network"|Remove-SCVMNetwork
+                                               ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ReadError: (:) [Remove-SCVMNetwork], CarmineException
    + FullyQualifiedErrorId : 15011,Microsoft.SystemCenter.VirtualMachineManager.Cmdlets.RemoveSCVMNetworkCmdlet

Resolve Error 15011

Run Virtual Machine Manager Console > Navigate to 'VMs and services' >  'VM Networks' > right-click the Logical Network > Properties > Depedencies > Delete all subnet dependencies > OK > right-click the Logical Network again > Delete > OK

Virtual Machine Manager (VMM) Error ID: 1730

Symptom:
$vmName='bad-guestvm'
$vm = Get-SCVirtualMachine -Name $vmName
Read-SCVirtualMachine -VM $vm

Read-SCVirtualMachine : The selected action could not be completed because the virtual machine is not in a state in
which the action is valid. (Error ID: 1730)

Check the state of the virtual machine, and verify that the selected job can be run on a virtual machine in that state.

To restart the job, run the following command:
PS> Restart-Job -Job (Get-VMMServer localhost | Get-Job | where { $_.ID -eq "{442cfeda-2df8-4ee8-9c3d-f36390653124}"})
At line:1 char:1
+ Read-SCVirtualMachine -VM $vm
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ReadError: (:) [Read-SCVirtualMachine], CarmineException
    + FullyQualifiedErrorId : 1730,Microsoft.SystemCenter.VirtualMachineManager.Cmdlets.RefreshVmCmdlet

Resolution: remove guest VM clustered role an then re-add it to the cluster
Resolution:
remove guest VM clustered role an then re-add it to the cluster

1. Remove Clustered Role

# Experimental: remove clustered roles via PowerShell
# Note: currently, these commands don't remove the roles properly; hence, the GUI method is necessary
$vmName='bad-guestvm0001'
$clusteredRoles=Get-ClusterResource -name *$vmName*
foreach($role in $clusteredRoles){
    $ownerNode=$role.OwnerNode
    $roleName=$role.Name
    write-warning "Remove-ClusterResource -Name $roleName -Force"
    pause
    if($env:computername -ne $role.OwnerNode){
        $session=new-pssession $ownerNode
        if($session.State -eq 'Opened'){            
            invoke-command -session $session {param($roleName)Remove-ClusterResource -Name $roleName -Force} -Args $roleName
            remove-pssession $session
        }
    }else{
        Remove-ClusterResource -Name $roleName -Force
    }
}

2. Re-add Guest VM to clustered role

# Required: console or RDP session onto the owner node

# Experimental: running this command via Remote WinRM has failed with this error
# WARNING: If you are running Windows PowerShell remotely, note that some failover clustering cmdlets do not work
# remotely. When possible, run the cmdlet locally and specify a remote computer as the target. To run the cmdlet
# remotely, try using the Credential Security Service Provider (CredSSP). All additional errors or warnings from this
# cmdlet might be caused by running it remotely.
# WARNING: You do not have administrative privileges on the cluster. Contact your network administrator to request
# access.
#     Access is denied
$vmName='bad-guestvm'
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
}

addVmToCluster $vmName

Repair with the Ignore Option:

Kubernetes Broken Due To Unknown Reasons

Problem 1: Admin User Unable to Login to Cluster via Controller (Master Node)

# SSL Error:
The connection to the server x.x.x.x:6443 was refused - did you specify the right host or port?

# Resolution to the SSL problem:
sudo -i
swapoff -a
exit
strace -eopenat kubectl version

# User privilege error:
kim@controller01:~$ kubectl cluster-info
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
error: You must be logged in to the server (Unauthorized)

kim@controller01:~$ kubectl version
Client Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.10", GitCommit:"8152330a2b6ca3621196e62966ef761b8f5a61bb", GitTreeState:"clean", BuildDate:"2021-08-11T18:06:15Z", GoVersion:"go1.15.15", Compiler:"gc", Platform:"linux/amd64"}
error: You must be logged in to the server (the server has asked for the client to provide credentials)

# Resolution to Admin user privilege error:
# Grant current user admin privileges on Kubernetes
# mkdir -p $HOME/.kube # this was done during previous setup
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
# sudo chown $(id -u):$(id -g) $HOME/.kube/config # this was done during previous setup

Problem 2: SSL Certificates Not Automatically Renewed

# Error message with kimconnect.com SSL Cert
Warning  Failed   84m (x328 over 13d)  cert-manager  The certificate request has failed to complete and will be retried: Failed to wait for order resource "kimconnect-cert-qlnl9-1800784958" to become ready: order is in "invalid" state:

kim@controller01:~$ k get certificaterequests.cert-manager.io
NAME                          READY   AGE
kimconnect-cert-jqlvf         True    90d
kimconnect-cert-qlnl9         False   30d

# Try to delete cert requests and secrets, and wait for cert to regenerate
k delete certificaterequests kimconnect-cert-qlnl9
k delete secret kimconnect-cert

# Try to force cert to renew before 1440 hours (immediately)
kubectl patch certificate kimconnect-cert --patch '
- op: replace
  path: /spec/renewBefore
  value: 1440h
' --type=json

# Wait for cert to become ready, then reverse the change
kubectl patch certificate kimconnect-cert --patch '
- op: remove
  path: /spec/renewBefore
' --type=json

# Worst case scenario, delete the cert and recreate it
k delete cert kimconnect-cert
cat <<EOF > kimconnect-cert.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: kimconnect-cert
  namespace: default
  annotations:
    kubernetes.io/ingress.class: "nginx"
    acme.cert-manager.io/http01-edit-in-place: "true"
    kubernetes.io/tls-acme: "true"
spec:
  dnsNames:
    - kimconnect.com
    - www.kimconnect.com
  secretName: kimconnect-cert
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
EOF
kubectl apply -f kimconnect-cert.yaml

PowerShell: Move Virtual Machine Storage Using VMM

# moveVmStorageUsingVmm.ps1
# Version 0.01

$vmNames=@(
  'TESTVM0001',  
  'TESTVM0002',
  'TESTVM0003'
)
$storageLocations=@(
  'C:\ClusterStorage\BLOB001',
  'C:\ClusterStorage\BLOB002',
  'C:\ClusterStorage\BLOB003'
)
$storageMaxPercent=79
$confirmation=$true

function moveVmStorageUsingVmm($vmName,$newStorage,$storageMaxPercent=80){  
  try{
    $vmHosts=Get-SCVMHost
    $vm=Get-SCVirtualMachine -Name $vmName
    if($vm.count -eq 1){
      $currentHost=$vm.Hostname
      $storage=Get-SCStorageVolume -VMHost $currentHost|?{$_.Name -eq $newStorage}
      $capacity=$storage.Capacity
      $freespace=$storage.FreeSpace
      $storageUtilizedPercent=[math]::round(($capacity-$freespace)/$capacity*100,2)
      $totalSize=0
      $disks = Get-SCVirtualDiskDrive -VM $vmname
      $disks.VirtualHardDisk.Size|%{$totalSize+=$_}      
      $projectedPercent=[math]::round(($capacity-$freespace+$totalSize)/$capacity*100,2)
      write-host "$newStorage current utilization percentage: $storageUtilizedPercent`% and projected: $projectedPercent`% after adding $([math]::round($totalSize/1GB,2))GB's"
      $storageFeasible=if($projectedPercent -lt $storageMaxPercent){$true}else{$false}
      if($storageFeasible){
        $vmHost=$vmHosts|?{$_.Name -eq $currentHost}
        Move-SCVirtualMachine -VM $vm -VMHost $vmHost -Path $newStorage -UseLAN -UseDiffDiskOptimization # -RunAsynchronously
        return $true
      }else{
        write-warning "Infeasible storage location: Available storage volume is $storageMaxPercent`% and projected is $projectedPercent`%"
        return $false
      }
    }else{
      write-warning "$vmName matches more than 1 guest VM's; hence, this item is skipped."
      return $null
    }
  }catch{
    write-warning $_
    return $false
  }
}

function moveStorage($vmNames,$storageLocations,$storageMaxPercent,$confirmation){
  $storageIndex=0
  $useSameStorage=$true
  if($vmNames.count -gt 1){
    for($i=0;$i -lt $vmNames.count;$i++){
      $vmName=if($useSameStorage){$vmNames[$i]}else{$vmNames[--$i]}
      try{
        $null=Get-SCVirtualMachine -Name $vmName|Read-SCVirtualMachine -ea Stop
        $storageLocation=if($useSameStorage){$storageLocations[$storageIndex]}else{$storageLocations[++$storageIndex]}
        if($storageLocation){
          if($confirmation){
            write-host "Move $vmName storage to $storageLocation`?"
            pause
          }else{
            write-host "Moving $vmName storage to $storageLocation ..."
          }
          $useSameStorage=moveVmStorageUsingVmm $vmName $storageLocation $storageMaxPercent
        }else{
          write-warning "Exhausted storage locations to move VM's"
          return $false
        }
      }catch{
        write-warning $_
        # Get-SCVirtualMachine -Name $vmName|Repair-SCVirtualMachine -force # I haven't tested this
        return $false
      }
    }
  }else{
    [string]$vmname=$vmNames
    try{
      $null=Get-SCVirtualMachine -Name $vmname|Read-SCVirtualMachine -ea Stop
      foreach($storageLocation in $storageLocations){
        if($storageLocation){
          if($confirmation){
            write-host "Move $vmname storage to $storageLocation`?"
            pause
          }else{
            write-host "Moving $vmname storage to $storageLocation ..."
          }
          $success=moveVmStorageUsingVmm $vmName $storageLocation $storageMaxPercent
          if($true -eq $success){
            return $true
          }else{
            write-warning "$vmname cannot be moved to $storageLocation"
          }
        }else{
          write-warning "Exhausted storage locations to move VM's"
          return $false
        }
      }      
    }catch{
      write-warning $_
      return $false
    }
  }
}

moveStorage $vmNames $storageLocations $storageMaxPercent $confirmation

Get Hyper-V Cluster Automatic Balancing Configurations

# getHyperVLoadBalanceMode.ps1

$clustername=(Get-Cluster).Name
function getHyperVLoadBalanceMode($clustername=(Get-Cluster).Name){

    $AutoBalancerLevel=[hashtable]@{
        '1'='Low | Move when host is more than 80% loaded (default)'
        '2'='Medium | Move when host is more than 70% loaded'
        '3'='High | Average nodes and move when host is more than 5% above average'
    }
    
    $AutoBalancerMode=[hashtable]@{
        '0'='Disabled'
        '1'='Load balance on node joins'
        '2'='Load balance on node joins and every 30 minutes (default)'
    }
    try{
        $level=(Get-Cluster $clustername).AutoBalancerLevel.toString()
        $levelDescription=$AutoBalancerLevel[$level]
        $mode=(Get-Cluster $clustername).AutoBalancerMode.toString()
        $modeDescription=$AutoBalancerMode[$mode]
        write-host "Clustername: $clusterName`r`n - AutoBalanceLevel: $level $levelDescription`r`n - AutoBalanceMode: $mode $modeDescription"
    }catch{
        write-warning $_
    }

}

getHyperVLoadBalanceMode $clustername
# Sample output
PS C:\Windows\system32> getHyperVLoadBalanceMode
Clustername: cluster1
 - AutoBalanceLevel: 1 (default) Low Move when host is more than 80% loaded
 - AutoBalanceMode: 2 (default) Load balance on node join and every 30 minutes

PowerShell: Create Hyper-V Guest VM From Virtual Disk (VHDX)

Part 1: Creating Hyper-V Guest VM From a Virtual Disk

# createHyperVGuestVmFromDisk.ps1
# Version 0.02
# The intent of this script is to create a Hyper-V Guest VM basing on existing backup VHDX file(s)

$sourceVhdx='\\FILESERVER008\_Images\Windows2019_Image.vhdx'
$destinationFolder='\\VIRTUALMACHINES\STAGE'
$network='External-Connection'
$vlan='3000'

$newVmName='TESTVM009'
$memory='8GB'
$cpus=4
$secureBoot=$true # Windows: True, Linux: False
$extraDisks=@($null) # $extraDisks=@('test')
$generation=2
$deleteSourceDisks=$true
$onlineVm=$true

function createHyperVGuestVmFromDisk{
  param(
      $sourceVhdx,
      $newVmName,
      $destinationFolder,
      $memory='4GB',
      $cpus=2,
      $network,
      $vlan,
      $extraDisks=$null,
      $generation=2,
      $secureBoot=$false,
      $onlineVm=$true,
      $deleteSourceDisks=$false
  )
  $ErrorActionPreference = 'stop'
  
  function confirmation($content,$testValue="I confirm",$maxAttempts=3){
    $confirmed=$false
    $cancelCondition=@('cancel','no','exit','nope')
    $attempts=0
    clear-host 
    write-host $($content|out-string).trim()
    write-host "`r`nPlease 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. Input CANCEL to skip this item";
        if ($userInput.ToLower() -eq $testValue.ToLower()){
            $confirmed=$true;
            write-host "Confirmed!`r`n";
            break;                
        }elseif($userInput.tolower() -in $cancelCondition){
            write-host 'Cancel command received.'
            $confirmed=$false
            break
        }else{
            clear-host
            $content|write-host
            write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
            }
        }
    return $confirmed
  }

  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 $generation `
              -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($extraDisks){
          for($i=0;$i -lt $extraDisks.count;$i++){
            $extraDisk=$extraDisks[$i]
            $isvalidDisk=if(test-path $extraDisk){$extraDisk}else{$null}
            if($isvalidDisk){
              $newDiskPath=(join-path $destinationFolder $vmName) + "\$vmName`_disk$($i+1).vmdk"
              if(!(test-path $newDiskPath)){
                New-Item -ItemType File -Path $newDiskPath -Force # touch before copying contents
                Copy-Item -Path $extraDisk -Destination $newDiskPath
              }else{
                  write-warning "Volume $newDiskPath already exists. Thus, that VMDK will be used instead of a copy."
              }              
              Add-VMHardDiskDrive -VMName $newVmName -Path $newDiskPath 
            }else{
              write-warning "Disk path '$extraDisk' in invalid."
            }
          }
      }else{
        write-host "$newVmName has no extra disks to attach."
      }
      $disksToRemove=[array]$extraDisks+$sourceVhdx|?{$_} # join string to array and remove empty entries
      foreach($diskToRemove in $disksToRemove){
        $confirmed=confirmation "Remove source disk $diskToRemove"
        if($confirmed){
          remove-item $diskToRemove -force
        }else{
          write-host "$diskToRemove NOT removed."
        }
      }
      if($onlineVm){
          start-vm $newVmName
      }
      return $true
  }catch{
      write-warning "$($error[0])"
      return $false
  }
}

createHyperVGuestVmFromDisk $sourceVhdx `
  $newVmName `
  $destinationFolder `
  $memory `
  $cpus `
  $network `
  $vlan `
  $extraDisks `
  $generation `
  $secureBoot `
  $onlineVm `
  $deleteSourceDisk

Part 2: Adding New VM to Cluster

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){
                $null=Add-ClusterVirtualMachineRole -Cluster $targetCluster -VirtualMachine $vmName -EA Stop
                $results+=[hashtable]@{$vmName=$true}
            }else{
                write-host "No clusters defined."
                $results+=[hashtable]@{$vmName=$false}
                }
            $moved=if(get-cluster -ea SilentlyContinue){Move-ClusterVirtualMachineRole $newVmName}else{$false}
            if($moved){write-host "'$newVmname' has been moved to $($moved.OwnerNode)"}
        }catch{
            write-warning "$($error[0])"
            $results+=[hashtable]@{$vmName=$false}
            }
    }
    return $results
}

addVmToCluster $newVmName

Possible Error Message:

Microsoft Hyper-V UEFI

Virtual Machine Boot Summary

1.SCSI Disk (0,0) the boot loader did not load an operating system
2. Network adapter (00155D406142) a boot image was not found

No operating system was loaded. Your virtual machine may be configured incorrectly. Exit and rec-configure your VM or click restart to retry the current boot sequence again.

Resolution:

The case where this error has been thrown has been associated with an incorrect virtual machine generation type. Hence, the resolution has been:

A. Convert Generation 2 machine type back to Generation 1 as the original source disk VM must match its re-creation.
B. The misconfigured VM must be ‘deleted’ and re-created as a Generation 1 VM.

# Converting Generation 2 virtual disk to Gen 1
$diskFile='\\VIRTUALMACHINES\HyperV\originalDisk_gen2.vhdx'
$fixedFile='\\VIRTUALMACHINES\HyperV\originalDisk_gen1.vhdx'
Convert-VHD -Path $diskFile -DestinationPath $fixedFile -VHDType Dynamic

PowerShell: Find Guest VMs Associated with a Certain Storage Path

# findGuestMvsByStorage.ps1

$storagePath='\\SMBSERVER009'

function getAllGuestVms($clusterName){
  try{
    Import-Module FailoverClusters
    $clusterName=if($clusterName){
      $clustername
    }else{
      (get-cluster).name
    }
    $allHyperVHosts={(Get-ClusterNode -Cluster $clusterName|?{ $_.State -eq "Up" }).Name | %{$_.ToLower()}}.Invoke()
    $allVms=foreach ($hyperVHost in $allHyperVHosts){
      invoke-command -computername $hyperVHost -scriptblock{
        write-host "Getting VM List on $env:computername";
        Get-VM |select-object Name,State,Status,Path
      }|select-object * -ExcludeProperty RunspaceId,PSShowComputerName
    }
    if($allVms){return $allVms}else{return $null}
  }catch{
    write-warning $_
    return $false
  }
}
function findGuestMvsByStorage{
  param (
    $storagePath,
    $clusterName=$null
    )
  try{
      $allVms=getAllGuestVms $clusterName
      if(!$allVms){return $null}
      $matchedStorage=$allVms|?{$_.Path -like "*$storagePath*"}
      $online=$matchedStorage|?{$_.State.Value -like 'Running*'}
      $offline=$matchedStorage|?{$_.State.Value -notlike 'Running*'}
      if($online){
        write-host "There are $($online.count) online VMs associated with '$storagePath'"
      }
      if($offline){
        write-host "There are $($offline.count) offlined VMs associated with '$storagePath'"
      }
      if($matchedStorage){
        $matchedStorage
        return $matchedStorage
      }else{
        write-host "'$storagePath' is not being associated with any VM in cluster '$((get-cluster).Name)'"
        return $null
      }
  }catch{
      write-warning $_
      return $false
  }
}

$result=findGuestMvsByStorage $storagePath
$result|select Name,State,PSComputerName
# Sample Output
PS C:\Windows\system32> $result=findGuestMvsByStorage $storagePath
Getting VM List on HYV01
Getting VM List on HYV02
Getting VM List on HYV03
Getting VM List on HYV04
Getting VM List on HYV05
Getting VM List on HYV06
Getting VM List on HYV07
Getting VM List on HYV08
Getting VM List on HYV09
Getting VM List on HYV10
Getting VM List on HYV11
Getting VM List on HYV12
Getting VM List on HYV13
Getting VM List on HYV14
Getting VM List on HYV15
Getting VM List on HYV16
Getting VM List on HYV17
Getting VM List on HYV18
Getting VM List on HYV19
There are 6 online VMs associated with '\\FILESERVER009'
There are 16 offlined VMs associated with '\\FILESERVER009'

PS C:\Windows\system32> $result|select Name,State
Name                           State
----                           -----
TESTVM01                       OffCritical
TESTVM02                       Off
TESTVM03                       OffCritical
TESTVM04                       OffCritical
TESTVM05                       RunningCritical

PowerShell: Restart a Service on All Hyper-V Hosts of a Cluster

$serviceName='vmms'
$clusterName='HyperV-cluster001'

function restartServiceAllClusterNodes($service='vmms',$clusterName){
  function restartService($serviceName){
    $waitSeconds=40
    $isValidProcess=try{[bool](get-service $serviceName -EA Stop)}catch{$false}
    if($isValidProcess){
        try{
            $process=Start-Process -FilePath powershell.exe -ArgumentList "-Command Restart-Service $serviceName" -PassThru -NoNewWindow
            $process|Wait-Process -Timeout $waitSeconds -ErrorAction Stop
            return $true
        }catch{
            write-warning $_
            $process|Stop-Process -Force
            $processId=(get-process $serviceName).Id
            if($processId){
                write-host "Program now forcefully kills PID $processId of process $serviceName"
                $null=$processId|%{taskkill /f /pid $_} # works more reliably than Stop-Process $processName -Force
                Start-Service $serviceName -ErrorAction Ignore
                $started=$(try{get-service $serviceName}catch{$false})
                if($started){
                    write-host "'serviceName' status is now $($started.Status)"
                    return $true
                }else{
                    write-warning "'serviceName' status is $($started.Status)"
                    return $false
                }
            }else{
                write-warning "Service '$serviceName' PID not found."
                return $false        
            }
        }
    }
  }

  $results=@()
  try{
    Import-Module FailoverClusters
    $clusterName=if($clusterName){
      invoke-command -computername $clustername {(get-cluster).name}
    }else{
      (get-cluster).name
    }
    $allHyperVHosts={(Get-ClusterNode -Cluster $clusterName|?{ $_.State -eq "Up" }).Name | %{$_.ToLower()}}.Invoke()
    foreach ($hyperVHost in $allHyperVHosts){
      $result=invoke-command -computername $hyperVHost -EA SilentlyContinue -scriptblock {
        param($restartService,$serviceName)
        [scriptblock]::create($restartService).invoke($servicename)
      } -Args ${function:restartService},$serviceName
      write-host "$hypervHost=$result"
      $results+=[pscustomobject]@{$hypervHost=$result}
    }
  }catch{
    write-warning $_    
  }
}

$results=restartServiceAllClusterNodes $serviceName $clusterName
$results

PowerShell: Find Hyper-V Host by Guest VM Name

# findVmHostByGuestName.ps1

$vmName='TESTVM'

function findVmHostByGuestName($vmName){
    try{
        Import-Module Hyper-V
        Import-Module FailoverClusters
        $allHyperVHosts={(Get-ClusterNode | Where { $_.State -eq "Up" }).Name | %{$_.ToLower()}}.Invoke()
        $allVms=foreach ($hyperVHost in $allHyperVHosts){invoke-command -computername $hyperVHost -scriptblock{write-host "Getting VM List on $env:computername";Get-VM |select Name,Path}|select-object * -ExcludeProperty RunspaceId,PSShowComputerName}
        $matchedHost=$allVms|?{$_.Name -like "*$vmName*"}
        if($matchedHost){
            return $matchedHost
        }else{
            write-host "'$vmName' is not found in cluster '$((get-cluster).Name)'"
            return $null
        }
    }catch{
        write-warning $_
        return $false
    }
}

findVmHostByGuestName $vmName

PowerShell: Get All Hyper-V Host Spectre Patch Versions

# getAllVmSpectrePatchVersions.ps1

function getHyperVHostsInForest{
    function includeRSAT{
        $ErrorActionPreference='stop'
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        #$rsatWindows7x32='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x86-RefreshPkg.msu'
        $rsatWindows7x64='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x64-RefreshPkg.msu'
        $rsatWindows81='https://download.microsoft.com/download/1/8/E/18EA4843-C596-4542-9236-DE46F780806E/Windows8.1-KB2693643-x64.msu'
        $rsat1709 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1709-x64.msu"
        $rsat1803 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1803-x64.msu"
        $rsatWs2016 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS2016-x64.msu"
   
        # This command does not work on Windows 2012R2
        #$releaseId=(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ReleaseId).ReleaseId
        #Get-ItemProperty : Property ReleaseId does not exist at path HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows
        #NT\CurrentVersion.
        #At line:1 char:2
        #+ (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Na ...
        #+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        #    + CategoryInfo          : InvalidArgument: (ReleaseId:String) [Get-ItemProperty], PSArgumentException
        #    + FullyQualifiedErrorId : System.Management.Automation.PSArgumentException,Microsoft.PowerShell.Commands.GetItemPropertyCommand
   
        $releaseId=(Get-Item "HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion").GetValue('ReleaseID')
        $osVersion=[System.Environment]::OSVersion.Version
        [double]$osVersionMajorMinor="$($osVersion.Major).$($osVersion.Minor)" 
        $osName=(Get-WmiObject Win32_OperatingSystem).Name
        #$osType=switch ((Get-CimInstance -ClassName Win32_OperatingSystem).ProductType){
        #    1 {'client'}
        #    2 {'domaincontroller'}
        #    3 {'memberserver'}
        #    }
   
        $windowsVersion=(Get-CimInstance Win32_OperatingSystem).Version
   
        switch ($releaseId){
            1607{write-host 'Windows Server 2016 Release 1607 detected';$link=$rsatWs2016;break}
            1709{write-host 'Windows Server 2016 Release 1709 detected';$link=$rsat1709;break}
            1803{write-host 'Windows Server 2016 Release 1803 detected';$link=$rsat1803}
        }
       
        switch ($osVersionMajorMinor){
            {$_ -eq 6.0}{write-host 'Windows Server 2008 or Windows Vista detected';$link=$rsat1709;break}
            {$_ -eq 6.1}{write-host 'Windows Server 2008 R2 or Windows 7 detected';$link=$rsatWindows7x64;break}
            {$_ -eq 6.2}{write-host 'Windows Server 2012 or Windows 8.1 detected';$link=$rsatWindows81;break}
            {$_ -eq 6.3}{write-host 'Windows Server 2012 R2 detected';$link=$rsatWindows81}
        }
  
        if (!(Get-Module -ListAvailable -Name ActiveDirectory -EA SilentlyContinue)){
            Write-host "Prerequisite checks: module ActiveDirectory NOT currently available on this system. Please wait while the program adds that plugin..."
            try{
                # If OS is Windows Server, then install RSAT using a different method
                if ($osName -match "^Microsoft Windows Server") {
                    # This sequence has confirmed to be valid on Windows Server 2008 R2 and above
                    Write-Verbose "Importing Windows Feature: RSAT-AD-PowerShell"
                    Import-Module ServerManager
                    Add-WindowsFeature RSAT-AD-PowerShell
                    }
                else{
                    Write-Verbose "This sequence targets Windows Client versions"
                    $destinationFile= ($ENV:USERPROFILE) + "\Downloads\" + (split-path $link -leaf)
                    Write-Host "Downloading RSAT from $link..."
                    Start-BitsTransfer -Source $link -Destination $destinationFile
                    $fileCheck=Get-AuthenticodeSignature $destinationFile
                    if($fileCheck.status -ne "valid") {write-host "$destinationFile is not valid. Please try again...";break}
                    $wusaCommand = $destinationFile + " /quiet"
                    Write-host "Installing RSAT - please wait..."
                    Start-Process -FilePath "C:\Windows\System32\wusa.exe" -ArgumentList $wusaCommand -Wait
                    }
                return $true
                }
            catch{
                write-warning "$($error[0].Exception)"
                return $false
                }
        }else{
            Write-host "Prerequisite checks: module ActiveDirectory IS currently available on this system." -ForegroundColor Green
            return $true
            }
    }
     function listAllHyperVNodes($verbose=$true){
        try{
            $timer=[System.Diagnostics.Stopwatch]::StartNew()
            $domains=(Get-ADForest).Name|%{(Get-ADForest -Identity $_).Name}
            foreach ($domain in $domains){
                #[string]$dc=(get-addomaincontroller -DomainName "$domain" -Discover -NextClosestSite).HostName
                write-host "Collecting all Hyper-V Clusters in $domain. This may take a while, depending on cluster sizes."
                $allClusters=(get-cluster -domain $domain).Name
                if($verbose){
                    $elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
                    write-host "Minutes elapsed $elapsed`: cluster names collected"
                    }

                $allHyperVNodes=@()
                foreach ($cluster in $allClusters){
                    $nodes=.{$x=Get-ClusterNode -Cluster $cluster -ea SilentlyContinue
                            if($x){
                                $x|Where-Object{$_.State -eq 'Up'}|Select-Object Name,@{name='Cluster';e={$cluster}}
                            }else{
                                $false
                            }
                            }
                    if($nodes){$allHyperVNodes+=$nodes}
                }
                if($verbose){
                    $elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
                    write-host "Minutes elapsed $elapsed`: Hyper Node names collected..."
                    }
                }                
            return $allHyperVNodes
        }catch{
            Write-Error $_
            return $false
            }
    }
 
    try{
        $null=includeRSAT;
        $hyperVHosts=listAllHyperVNodes
        #$hyperVHostNames=sortArrayStringAsNumbers $hyperVHosts
        $hyperVHostNames=$hyperVHosts|sort -property Cluster
        return $hyperVHostNames
    }catch{
        Write-Error $_
        return $false
        }
    }

function pickList($list){
    # Although it's more efficient to obtain the index and set it as display,
    # humans prefer see a list that starts with 1, instead of 0
    $display=for ($i=0;$i -lt $list.count;$i++){
        "$($i+1)`:`t$($list[$i])`r`n";
        }
    $lines=($display | Measure-Object -Line).Lines
    write-host $($display)
    $maxAttempts=3
    $attempts=0;
    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "Attempt number $maxAttempts of $maxAttempts. Exiting loop..`r`n"
            break;
            }
        $userInput = Read-Host -Prompt 'Please pick a number from the list above';
        try {
            $value=[int]$userInput;
            }catch{
                $value=-1;
                }
        if ($value -lt 1 -OR $value -gt $lines){
            cls;
            write-host "Attempt number $attempts of $maxAttempts`: $userInput is an invalid value. Try again..`r`n"
            write-host $display
            }else{
                $item=$list[$value-1];
                write-host "$userInput corresponds to $item`r`n";
                return $item
                }
        }     
    }
function selectCluster($clusters){
    # Requires function named pickList
    $uniqueClusters=$clusters|select -unique
    $pickedCluster=$(pickList $uniqueClusters)
    if($pickedCluster){
        return $pickedCluster
    }else{
        write-warning 'No clusternames were picked.'
        return $false
    }
}

function pickHost($hosts){
    # Requires function named pickList
    $pickedHost=$(pickList $hosts)
    if($pickedHost){
        return $pickedHost
    }else{
        write-warning 'No clusternames were picked.'
        return $false
    }
}
function sortArrayStringAsNumbers([string[]]$names){
    $hashTable=@{}
    foreach ($name in $names){
        #[int]$x=.{[void]($name -match '(?:.(\d+))+$');$matches[1]}
        #$x=.{[void]($name -match '(?:.(\d+)+)$');@($name.substring(0,$name.length-$matches[1].length),$matches[1])}
        $x=.{[void]($name -match '(?:.(\d+)+)$');($name.substring(0,$name.length-$matches[1].length))+$matches[1].PadLeft(8,'0')}
        $hashTable.Add($name,$x)
        }
    $sorted=foreach($item in $hashTable.GetEnumerator() | Sort Value){$item.Name}
    return $sorted
}

write-host "Obtaining cluster names and associated hosts..."
$hyperVHostsInForest=getHyperVHostsInForest
$pickedCluster=selectCluster $hyperVHostsInForest.Cluster
$pickedHosts=$hyperVHostsInForest|?{$_.Cluster -eq $pickedCluster}
$pickedHyperVHosts=sortArrayStringAsNumbers $pickedHosts.Name

function getSpectrePatchingVersions($hyperVHosts){
    write-host "Now obtaining Spectre Patching versions of Hyper-V Hosts..."
    $results=@{}
    foreach ($hyperVHost in $hyperVHosts){
        $spectrePatchingVersion=(Get-WmiObject -ComputerName $hyperVHost -ClassName Win32_BIOS).SMBIOSBIOSVersion
        write-host "$hyperVHost`: $spectrePatchingVersion"
        $results+=@{$hyperVHost=$spectrePatchingVersion}
    }
    return $results
}
getSpectrePatchingVersions $pickedHyperVHosts
function setVmMigrationPerformanceRemote($hyperVHosts){
    function setVmMigrationPerformance{
        param($performanceOption='TCPIP') # TCP is most compatible, and the other options are SMB and Compression
        $ErrorActionPreference='stop'
        try{
            Set-VMHost -VirtualMachineMigrationPerformanceOption $performanceOption
            return $true
        }catch{
            return $false
        }
    }
    $results=@{}
    $performanceOption='TCPIP'
    foreach ($hyperVHost in $hyperVHosts){
        try{
            $result=invoke-command -ComputerName $hyperVHost -ScriptBlock {
                param($setVmMigrationPerformance,$performanceOption)
                [scriptblock]::create($setVmMigrationPerformance).invoke($performanceOption)
            } -Args ${function:setVmMigrationPerformance},$performanceOption
            if($result){
                write-host "$hyperVHost`: $performanceOption"
                $results+=@{$hyperVHost=$performanceOption}
            }
        }catch{
            write-warning $_
        }
    }
    return $results
}
setVmMigrationPerformanceRemote $pickedHyperVHosts

PowerShell: Fix All VMs CPU Compatibility Setting

# fixAllVmCpuCompatibility.ps1
# version 0.01
function getHyperVHostsInForest{
    function includeRSAT{
        $ErrorActionPreference='stop'
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        #$rsatWindows7x32='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x86-RefreshPkg.msu'
        $rsatWindows7x64='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x64-RefreshPkg.msu'
        $rsatWindows81='https://download.microsoft.com/download/1/8/E/18EA4843-C596-4542-9236-DE46F780806E/Windows8.1-KB2693643-x64.msu'
        $rsat1709 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1709-x64.msu"
        $rsat1803 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1803-x64.msu"
        $rsatWs2016 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS2016-x64.msu"
   
        # This command does not work on Windows 2012R2
        #$releaseId=(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ReleaseId).ReleaseId
        #Get-ItemProperty : Property ReleaseId does not exist at path HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows
        #NT\CurrentVersion.
        #At line:1 char:2
        #+ (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Na ...
        #+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        #    + CategoryInfo          : InvalidArgument: (ReleaseId:String) [Get-ItemProperty], PSArgumentException
        #    + FullyQualifiedErrorId : System.Management.Automation.PSArgumentException,Microsoft.PowerShell.Commands.GetItemPropertyCommand
   
        $releaseId=(Get-Item "HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion").GetValue('ReleaseID')
        $osVersion=[System.Environment]::OSVersion.Version
        [double]$osVersionMajorMinor="$($osVersion.Major).$($osVersion.Minor)" 
        $osName=(Get-WmiObject Win32_OperatingSystem).Name
        #$osType=switch ((Get-CimInstance -ClassName Win32_OperatingSystem).ProductType){
        #    1 {'client'}
        #    2 {'domaincontroller'}
        #    3 {'memberserver'}
        #    }
   
        $windowsVersion=(Get-CimInstance Win32_OperatingSystem).Version
   
        switch ($releaseId){
            1607{write-host 'Windows Server 2016 Release 1607 detected';$link=$rsatWs2016;break}
            1709{write-host 'Windows Server 2016 Release 1709 detected';$link=$rsat1709;break}
            1803{write-host 'Windows Server 2016 Release 1803 detected';$link=$rsat1803}
        }
       
        switch ($osVersionMajorMinor){
            {$_ -eq 6.0}{write-host 'Windows Server 2008 or Windows Vista detected';$link=$rsat1709;break}
            {$_ -eq 6.1}{write-host 'Windows Server 2008 R2 or Windows 7 detected';$link=$rsatWindows7x64;break}
            {$_ -eq 6.2}{write-host 'Windows Server 2012 or Windows 8.1 detected';$link=$rsatWindows81;break}
            {$_ -eq 6.3}{write-host 'Windows Server 2012 R2 detected';$link=$rsatWindows81}
        }
  
        if (!(Get-Module -ListAvailable -Name ActiveDirectory -EA SilentlyContinue)){
            Write-host "Prerequisite checks: module ActiveDirectory NOT currently available on this system. Please wait while the program adds that plugin..."
            try{
                # If OS is Windows Server, then install RSAT using a different method
                if ($osName -match "^Microsoft Windows Server") {
                    # This sequence has confirmed to be valid on Windows Server 2008 R2 and above
                    Write-Verbose "Importing Windows Feature: RSAT-AD-PowerShell"
                    Import-Module ServerManager
                    Add-WindowsFeature RSAT-AD-PowerShell
                    }
                else{
                    Write-Verbose "This sequence targets Windows Client versions"
                    $destinationFile= ($ENV:USERPROFILE) + "\Downloads\" + (split-path $link -leaf)
                    Write-Host "Downloading RSAT from $link..."
                    Start-BitsTransfer -Source $link -Destination $destinationFile
                    $fileCheck=Get-AuthenticodeSignature $destinationFile
                    if($fileCheck.status -ne "valid") {write-host "$destinationFile is not valid. Please try again...";break}
                    $wusaCommand = $destinationFile + " /quiet"
                    Write-host "Installing RSAT - please wait..."
                    Start-Process -FilePath "C:\Windows\System32\wusa.exe" -ArgumentList $wusaCommand -Wait
                    }
                return $true
                }
            catch{
                write-warning "$($error[0].Exception)"
                return $false
                }
        }else{
            Write-host "Prerequisite checks: module ActiveDirectory IS currently available on this system." -ForegroundColor Green
            return $true
            }
    }
     function listAllHyperVNodes($verbose=$true){
        try{
            $timer=[System.Diagnostics.Stopwatch]::StartNew()
            $domains=(Get-ADForest).Name|%{(Get-ADForest -Identity $_).Name}
            foreach ($domain in $domains){
                #[string]$dc=(get-addomaincontroller -DomainName "$domain" -Discover -NextClosestSite).HostName
                write-host "Collecting all Hyper-V Clusters in $domain. This may take a while, depending on cluster sizes."
                $allClusters=(get-cluster -domain $domain).Name
                if($verbose){
                    $elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
                    write-host "Minutes elapsed $elapsed`: cluster names collected"
                    }

                $allHyperVNodes=@()
                foreach ($cluster in $allClusters){
                    $nodes=.{$x=Get-ClusterNode -Cluster $cluster -ea SilentlyContinue
                            if($x){
                                $x|Where-Object{$_.State -eq 'Up'}|Select-Object Name,@{name='Cluster';e={$cluster}}
                            }else{
                                $false
                            }
                            }
                    if($nodes){$allHyperVNodes+=$nodes}
                }
                if($verbose){
                    $elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
                    write-host "Minutes elapsed $elapsed`: Hyper Node names collected..."
                    }
                }                
            return $allHyperVNodes
        }catch{
            Write-Error $_
            return $false
            }
    }
 
    try{
        $null=includeRSAT;
        $hyperVHosts=listAllHyperVNodes
        #$hyperVHostNames=sortArrayStringAsNumbers $hyperVHosts
        $hyperVHostNames=$hyperVHosts|sort -property Cluster
        return $hyperVHostNames
    }catch{
        Write-Error $_
        return $false
        }
    }

function pickList($list){
    # Although it's more efficient to obtain the index and set it as display,
    # humans prefer see a list that starts with 1, instead of 0
    $display=for ($i=0;$i -lt $list.count;$i++){
        "$($i+1)`:`t$($list[$i])`r`n";
        }
    $lines=($display | Measure-Object -Line).Lines
    write-host $($display)
    $maxAttempts=3
    $attempts=0;
    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "Attempt number $maxAttempts of $maxAttempts. Exiting loop..`r`n"
            break;
            }
        $userInput = Read-Host -Prompt 'Please pick a number from the list above';
        try {
            $value=[int]$userInput;
            }catch{
                $value=-1;
                }
        if ($value -lt 1 -OR $value -gt $lines){
            cls;
            write-host "Attempt number $attempts of $maxAttempts`: $userInput is an invalid value. Try again..`r`n"
            write-host $display
            }else{
                $item=$list[$value-1];
                write-host "$userInput corresponds to $item`r`n";
                return $item
                }
        }     
    }
function selectCluster($clusters){
    # Requires function named pickList
    $uniqueClusters=$clusters|select -unique
    $pickedCluster=$(pickList $uniqueClusters)
    if($pickedCluster){
        return $pickedCluster
    }else{
        write-warning 'No clusternames were picked.'
        return $false
    }
}

function pickHost($hosts){
    # Requires function named pickList
    $pickedHost=$(pickList $hosts)
    if($pickedHost){
        return $pickedHost
    }else{
        write-warning 'No clusternames were picked.'
        return $false
    }
}
function sortArrayStringAsNumbers([string[]]$names){
    $hashTable=@{}
    foreach ($name in $names){
        #[int]$x=.{[void]($name -match '(?:.(\d+))+$');$matches[1]}
        #$x=.{[void]($name -match '(?:.(\d+)+)$');@($name.substring(0,$name.length-$matches[1].length),$matches[1])}
        $x=.{[void]($name -match '(?:.(\d+)+)$');($name.substring(0,$name.length-$matches[1].length))+$matches[1].PadLeft(8,'0')}
        $hashTable.Add($name,$x)
        }
    $sorted=foreach($item in $hashTable.GetEnumerator() | Sort Value){$item.Name}
    return $sorted
}

function enableCpuCompatibility($vmName){
    $compatibilityForMigration=(Get-VMProcessor $vmName).CompatibilityForMigrationEnabled
    if(!$compatibilityForMigration){    
        $vmIsRunning=(get-vm $vmname).State -eq 'Running'
        if($vmIsRunning){stop-vm $vmName}
        Set-VMProcessor $vmName -CompatibilityForMigrationEnabled 1
        if($vmIsRunning){start-vm $vmName}
    }else{
        write-host "$vmName already has CPU CompatibilityForMigrationEnabled set to True"
    }
}

write-host "Obtaining cluster names and associated hosts..."
$hyperVHostsInForest=getHyperVHostsInForest
$pickedCluster=selectCluster $hyperVHostsInForest.Cluster
$pickedHosts=$hyperVHostsInForest|?{$_.Cluster -eq $pickedCluster}
$pickedHyperVHosts=sortArrayStringAsNumbers $pickedHosts.Name
$results=@()
foreach ($hyperVHost in $pickedHyperVHosts){
    write-host "Performing discovery on $hyperVhost..."
    $getVMCpuCompatiblity={
        Get-VMProcessor *|Select-Object VMName,CompatibilityForMigrationEnabled,@{name='Online';e={if((get-vm $_.VMName).State -eq 'Running'){$true}else{$false}}}
    }
    $result=invoke-command -ComputerName $hyperVHost -ScriptBlock $getVMCpuCompatiblity|Select-Object -Property * -ExcludeProperty RunspaceId
    write-host ($result|out-string).trim()
    $results+=$result
}
$negatives=$results|?{$_.CompatibilityForMigrationEnabled -eq $false -and $_.Online -eq $true}

# Fix all items
foreach ($item in $negatives){
    $vmHost=$item.PSComputerName
    $vmName=$item.VMName
    invoke-command -computername $vmHost -scriptblock {
        param($enableCpuCompatibility,$vmName)
        [scriptblock]::create($enableCpuCompatibility).invoke($vmName)
    } -Args ${function:enableCpuCompatibility},$vmName
}
# Fix each item
$vmName='TESTVM008'
$vmHost=($negatives|?{$_.VMName -eq $vmName}).PSComputerName
if($vmHost){
    invoke-command -computername $vmHost -scriptblock {
        param($vmName)$vmIsRunning=(get-vm $vmname).State -eq 'Running'
        if($vmIsRunning){stop-vm $vmName -Force}
        Set-VMProcessor $vmName -CompatibilityForMigrationEnabled 1
        if($vmIsRunning){start-vm $vmName}
    } -Args $vmName
}else{
    write-host "VMName $vmName doesn't match an existing record."
}

PowerShell: Search for Hyper-V Guest VM That Has Not Been Registered In Cluster

# findVmHost.ps1

$vmName='TESTVM01'

function findVmHost($vmName){
    try{
        Import-Module Hyper-V
        Import-Module FailoverClusters
        $allHyperVHosts={(Get-ClusterNode | Where { $_.State –eq "Up" }).Name | %{$_.ToLower()}}.Invoke()
        $allVms=foreach ($hyperVHost in $allHyperVHosts){invoke-command -computername $hyperVHost -scriptblock{write-host "Getting VM List on $env:computername";Get-VM |select Name,Path}|select-object * -ExcludeProperty RunspaceId,PSShowComputerName}
        $matchedHost=$allVms|?{$_.Name -like "*$vmName*"}
        if($matchedHost){
            return $matchedHost
        }else{
            write-host "'$vmName' is not found in cluster '$((get-cluster).Name)'"
            return $null
        }
    }catch{
        write-warning $_
        return $false
    }
}

findVmHost $vmName

Resolving Guest Virtual Machine Critical Status in Hyper-V Manager

Part A: Validating problem as VM in Critical Status

# Check VM Status in Hyper-V
$vmName='TESTVM.kimconnect.com'
get-vm $vmName

# Sample output
Name                      State       CPUUsage(%) MemoryAssigned(M) Uptime   Status
----                      -----       ----------- ----------------- ------   ------
TESTVM.kimconnect.com     OffCritical 0           0                 00:00:00 Cannot connect to virtual machine confi...

# Check storage locations
Get-VM $vmName | fl *Location, Path

# Sample output
CheckpointFileLocation : \\NAS\VMS\TESTVM.kimconnect.com
ConfigurationLocation  : \\NAS\VMS\TESTVM.kimconnect.com\config
SnapshotFileLocation   : \\NAS\VMS\TESTVM.kimconnect.com
Path                   : \\NAS\VMS\TESTVM.kimconnect.com\config

Part B: Refresh Hyper-V Virtual Machine Management Service

# Restart Virtual Machine Management Service from VMM Server
$vmName='TESTVM.kimconnect.com'
$vmInfo=Get-SCVirtualMachine -Name $vmName
#$badVmConfig=$vmInfo|?{$_.Status -eq 'IncompleteVMConfig'}
$currentHosts=$vmInfo.VMHost.FullyQualifiedDomainName

function restartService($serviceName){
    $waitSeconds=40
    $isValidProcess=try{[bool](get-service $serviceName -EA Stop)}catch{$false}
    if($isValidProcess){
        try{
            $process=Start-Process -FilePath powershell.exe -ArgumentList "-Command Restart-Service $serviceName" -PassThru -NoNewWindow
            $process|Wait-Process -Timeout $waitSeconds -ErrorAction Stop
            return $true
        }catch{
            write-warning $_
            $process|Stop-Process -Force
            $processId=(get-process $serviceName).Id
            if($processId){
                write-host "Program now forcefully kills PID $processId of process $serviceName"
                $null=$processId|%{taskkill /f /pid $_} # works more reliably than Stop-Process $processName -Force
                Start-Service $serviceName -ErrorAction Ignore
                $started=$(try{get-service $serviceName}catch{$false})
                if($started){
                    write-host "'serviceName' status is now $($started.Status)"
                    return $true
                }else{
                    write-warning "'serviceName' status is $($started.Status)"
                    return $false
                }
            }else{
                write-warning "Service '$serviceName' PID not found."
                return $false         
            }
        }
    }
}

$currentHosts|%{invoke-command -computername $_ {param($restartService,$serviceName);write-host "$env:computername";[scriptblock]::create($restartService).invoke($serviceName)}} -Args ${function:restartService},'vmms'

Part C: Guest VM shows as online

# Checking VM while login to Hyper-V Host
PS C:\Windows\system32> get-vm $vmName
get-vm : Hyper-V was unable to find a virtual machine with name "TESTVM.kimconnect.com".
At line:1 char:1
+ get-vm $vmName
+ ~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (TESTVM.kimconnect.com:String) [Get-VM], VirtualizationException
    + FullyQualifiedErrorId : InvalidParameter,Microsoft.HyperV.PowerShell.Commands.GetVM

# Checking VM while login to VMM
PS C:\Windows\system32> (Get-SCVirtualMachine -Name $vmName).VMHost.FullyQualifiedDomainName
HYPERV01
HYPERV07 (bad record)

# Find bad VM config on VMM Server
$vmName='TESTVM.kimconnect.com'
$vmInfo=Get-SCVirtualMachine -Name $vmName
$badVmConfig=$vmInfo|?{$_.Status -eq 'IncompleteVMConfig'}
$badVmConfig|write-host

# Example of failed repair
Repair-SCVirtualMachine : This action is not valid because virtual machine TESTVM.kimconnect.com is in state
Incomplete VM Configuration. The repair action can only be used on a virtual machine in a failed state. (Error ID: 693)

At line:1 char:1
+ Repair-SCVirtualMachine -VM $badRecord -Retry
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ReadError: (:) [Repair-SCVirtualMachine], CarmineException
    + FullyQualifiedErrorId : 693,Microsoft.SystemCenter.VirtualMachineManager.Cmdlets.RepairVmCmdlet

PS C:\Windows\system32> refresh-vm $badRecord
refresh-vm : An internal error has occurred trying to contact the 'HYPERV01' server: NO_PARAM: NO_PARAM.
WinRM: URL: [http://HYPERV01:5985], Verb: [ENUMERATE], Resource:
[http://schemas.microsoft.com/wbem/wsman/1/wmi/root/microsoft/windows/storage/MSFT_Volume], Filter: []
 (Error ID: 2912, Detailed Error: The requested resource is in use (0x800700AA))

Check that WS-Management service is installed and running on server 'HYPERV01'. For more information
use the command "winrm helpmsg hresult". If 'HYPERV01' is a host/library/update server or a PXE server
role then ensure that VMM agent is installed and running. Refer to http://support.microsoft.com/kb/2742275 for more
details.

To restart the job, run the following command:
PS> Restart-Job -Job (Get-VMMServer localhost | Get-Job | where { $_.ID -eq "{26ce1506-da92-42a7-a400-705fe1008188}"})
At line:1 char:1
+ refresh-vm $badRecord
+ ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ReadError: (:) [Read-SCVirtualMachine], CarmineException
    + FullyQualifiedErrorId : 2912,Microsoft.SystemCenter.VirtualMachineManager.Cmdlets.RefreshVmCmdlet

Fixing an Issue On Windows Server 2019 Hyper-V with Uneven Distribution of Available CPU Cores

Issue:

When guest-VMs are being migrated between Hyper-V Hosts within a cluster, CPU core scheduling seems to have been disproportionately distributed toward even-numbered cores (0,2,4,6,8, etc.)

Cause:

There appears to be a new type of processor scheduling named ‘core scheduler’ in Windows Server 2019 that succeeds the previous versions of Windows of ‘classic scheduler’. The difference between those two types of scheduling would affect how migrating VMs would be pinned toward certain numbered CPUs. In the screenshot above, odd-numbered CPU cores seem to be excluded from those migrated VMs.

Resolution:

Check VMHost Supported Versions (notice the IsDefault field):

PS C:\Windows\system32> Get-VMHostSupportedVersion

Name Version IsDefault
---- ------- ---------
Microsoft Windows 8.1/Server 2012 R2 5.0 False
Microsoft Windows 10 1507/Server 2016 Technical Preview 3 6.2 False
Microsoft Windows 10 1511/Server 2016 Technical Preview 4 7.0 False
Microsoft Windows Server 2016 Technical Preview 5 7.1 False
Microsoft Windows 10 Anniversary Update/Server 2016 8.0 False
Microsoft Windows 10 Creators Update 8.1 False
Microsoft Windows 10 Fall Creators Update/Server 1709 8.2 False
Microsoft Windows 10 April 2018 Update/Server 1803 8.3 False
Microsoft Windows 10 October 2018 Update/Server 2019 9.0 True

Check Guest VMs Configured Versions:

PS C:\Windows\system32> Get-VM | FT Name,Version,State

Name Version State
---- ------- -----
TESTVM01 8.0 Off
TESTVM02 8.0 Running
TESTVM03 9.0 Off
TESTVM04 9.0 Running

Perform ‘whole-sale’ Updates:
Note: only ‘offline’ guest VMs will be able to update. Any running VMs will throw errors upon invoking the Update-VMVersion command

PS C:\WINDOWS\system32> Get-VM | Update-VMVersion

Confirm
Are you sure you want to perform this action?
Performing a configuration version update of "TESTVM01" will prevent it from being migrated to or imported on previous
versions of Windows. This operation is not reversible.

[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): A
Update-VMVersion : The operation cannot be performed while the virtual machine is in its current state.
At line:1 char:10
+ Get-VM | Update-VMVersion
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (VirtualMachine ...-5cb7879f5c4f']:VirtualMachine) [Update-VMVersion],
VirtualizationException
+ FullyQualifiedErrorId : InvalidState,Microsoft.HyperV.PowerShell.Commands.UpdateVMVersion

Check VM Processor’s Hardware Thread Counts:

PS C:\WINDOWS\system32> Get-VM | Get-VMProcessor | FT VMName,HwThreadCountPerCore

VMName HwThreadCountPerCore
------ --------------------
TESTVM01 1
TESTVM01 1
TESTVM01 1
TESTVM01 1

Fix the CPU Core Scheduling Affinity:
Note: the below command only affects off-lined guest VMs

PS C:\WINDOWS\system32> Get-VM | Set-VMProcessor -HwThreadCountPerCore 0
Set-VMProcessor : Failed to modify device 'Processor'.
Cannot change the processor functionality of a virtual machine now.
'TESTVM02' failed to modify device 'Processor'. (Virtual machine ID C99B-4469-A0E7)
Cannot change the processor functionality of virtual machine 'TESTVM02' while it is running. (Virtual machine ID
C99B-4469-A0E7)
At line:1 char:10
+ Get-VM | Set-VMProcessor -HwThreadCountPerCore 0
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Set-VMProcessor], VirtualizationException
+ FullyQualifiedErrorId : InvalidState,Microsoft.HyperV.PowerShell.Commands.SetVMProcessor

Generalized Approach:

To fix all guest VMs that have a lowered version of VM Host CPU support, each of the VM must be turned off. If that is not convenient due to production impacts, one could run this command to only target off-lined VMs:

Get-VM |?{$_.Version -lt 9.0}|Update-VMVersion -Force -EA SilentlyContinue|?{$_.State -eq 'Off'}|Set-VMProcessor -HwThreadCountPerCore 0

In our environment, the VM update command has no adverse effects on the performance of the machines thereafter. More importantly, the guest VMs have powered on without errors triggered by these changes. Also, it appears that version 9.0 guest VMs may have been patched to optimally allocate CPU resources. Hence, the Set-VMProcessor -HwThreadCountPerCore 0 may be unnecessary as of this writing. Still, setting that value as zero is recommended for consistency.

Result:

Screenshot of a more balanced CPU Core Scheduling