# 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