VMware Cloud Foundation 9.1

Improving CPU Detection in the ESXi Upgrade Compatibility Tool

Posted by

When I published the ESXi Upgrade Compatibility Tool, the goal was straightforward.

Automatically validate whether hosts are ready to move from ESXi 8.0 U3 to ESXi 9.x by checking:

  • CPU support
  • PCI hardware compatibility
  • Driver requirements
  • Firmware overlap

For most environments, the script worked exactly as intended.

Until I started testing it against a wider range of hardware platforms.

Problem

The hardware validation part of the script was reliable because it uses PCI identifiers:

  • Vendor ID
  • Device ID
  • SubVendor ID
  • SubDevice ID

Those identifiers are precise and map directly to entries in the Broadcom Compatibility Guide.

The CPU validation was a different story.

The original implementation tried to identify the correct Broadcom CPU Series by taking the CPU model name, removing some generic words, splitting the remaining text into individual words, and then looking for partial matches in the Broadcom CPU master list.

In many cases this worked.

But not always.

Some CPU families use naming conventions that do not map cleanly to the Broadcom CPU Series names.

For example:

Intel Xeon Gold 6338

Broadcom lists this as:

Intel Xeon Gold 6300/5300 (Ice-Lake-SP) Series

Likewise:

AMD EPYC 9454P

is represented as:

AMD EPYC 9004 Series

The original matching logic could sometimes fail to identify the correct CPU Series and therefore incorrectly report that no compatibility information was available.

And obviously, that is not something you want when preparing for a major upgrade.

Troubleshooting

The problem was not retrieving CPU information.

That part was easy:

$ESXiHost.ExtensionData.Summary.Hardware.CpuModel

The real challenge was translation.

The CPU names returned by ESXi are marketing names.

The Broadcom Compatibility Guide often categorizes processors by:

  • generation
  • series
  • codename
  • SKU ranges

Those are not always directly visible in the ESXi CPU model string.

After looking through multiple CPU families, it became clear that simple word matching was not sufficient.

A more intelligent matching mechanism was needed.

Solution

The script now uses a scoring-based matching engine.

Instead of trying to find one direct text match, the script extracts hints from the CPU model and assigns scores to potential Broadcom CPU Series candidates.

The matching process now considers:

  • vendor
  • product line
  • model number
  • SKU prefixes
  • processor codenames
  • expected Broadcom naming patterns

For AMD EPYC processors, the script derives the Broadcom series from the model number.

For example:

AMD EPYC 9454P

is interpreted as:

AMD EPYC 9004 Series

Another example:

AMD EPYC 7763

becomes:

AMD EPYC 7003/7003X Series

For Intel Xeon processors, the script uses SKU prefixes and known platform codenames.

For example:

Intel Xeon Gold 6338

is mapped to:

Intel Xeon Gold 6300/5300 (Ice-Lake-SP) Series

The same approach also works for newer processor generations such as:

  • Sapphire Rapids
  • Emerald Rapids
  • Cascade Lake
  • Skylake

Each potential Broadcom CPU Series receives a score.

The candidate with the highest confidence score is selected.

Only matches above a defined threshold are accepted.

This significantly reduces the chance of selecting the wrong CPU Series.

Example

During execution the script now reports which Broadcom CPU Series was selected:

>>> Processing Host: esx01.lab.local 
VCG CPU Series match: Intel Xeon Gold 6300/5300 (Ice-Lake-SP) Series 
Checking 8.0 U3 for CPU: Xeon Gold 6338... [SUPPORTED] 
Checking 9.0 for CPU: Xeon Gold 6338... [SUPPORTED]

If no reliable match can be found, the script now reports that explicitly:

WARNING: 
Geen passende VCG CPU Series gevonden voor CPU: 
AMD EPYC XXXX

Including the hints that were attempted.

This makes troubleshooting considerably easier than simply receiving an empty result.

Additional Improvements

I also took the opportunity to improve some other parts of the script.

The original version contained a hardcoded vCenter connection:

Connect-VIServer -Server "XXXXX"

The script now uses parameters:

.\vcf_upgrade_check.ps1 ` 
-vCenterName "vcenter.domain.local" ` 
-TargetVersion "9.0"

This makes the script much easier to reuse across environments and also allows future ESXi target versions without modifying the code itself.

Benefits

The improved CPU matching provides several advantages:

  • Better CPU Series detection
  • Reduced false negatives
  • Support for newer Intel and AMD processor generations
  • More transparent troubleshooting output
  • Reusable target version parameterization
  • More reliable upgrade readiness assessments

Most importantly, it improves confidence in the generated reports.

Final Thoughts

The difficult part of upgrade readiness is usually not collecting data.

It is interpreting it correctly.

PCI devices are relatively straightforward because they have unique identifiers.

CPUs are different. The same processor family can be represented in several ways depending on the vendor naming convention and how the Compatibility Guide groups processor generations.

By moving from simple word matching to a scoring-based detection model, the ESXi Upgrade Compatibility Tool now produces much more reliable CPU compatibility results.

And when you are planning a transition from ESXi 8.0 U3 to ESXi 9.x, having accurate CPU validation is exactly the kind of information you want before the actual upgrade window begins.

Script

# ==============================================================================================
# ESXi Upgrade Compatibility Tool (8.0 U3 -> 9.x) - Inclusief CPU & Correcte Strategie
# ==============================================================================================

param(
    [Parameter(Mandatory = $true)]
    [string]$vCenterName,

    [Parameter(Mandatory = $true)]
    [string]$TargetVersion
)

function Get-ClusterHosts {
    param([string]$ClusterName)
    $Cluster = Get-Cluster -Name $ClusterName -ErrorAction SilentlyContinue
    if (-not $Cluster) {
        Write-Error "Cluster '$ClusterName' niet gevonden!"
        return $null
    }
    return Get-VMHost -Location $Cluster | Where-Object { $_.ConnectionState -eq "Connected" }
}

function Get-VCGComparison {
    param(
        [string]$VID, [string]$DID, [string]$SVID, [string]$SDID,
        [string]$DeviceName, [string]$Ver, [string]$HostName
    )

    $Uri = "https://compatibilityguide.broadcom.com/compguide/programs/viewResults?limit=10&page=1"
    
    $BodyObj = @{
        programId = "io"
        filters   = @(
            @{ displayKey = "vid"; filterValues = @($VID) },
            @{ displayKey = "did"; filterValues = @($DID) },
            @{ displayKey = "svid"; filterValues = @($SVID) },
            @{ displayKey = "maxSsid"; filterValues = @($SDID) },
            @{ displayKey = "productReleaseVersion"; filterValues = @("ESXi $Ver") }
        )
        keyword = @()
        date    = @{ startDate = $null; endDate = $null }
    }
    
    $JsonBody = $BodyObj | ConvertTo-Json -Depth 10

    try {
        $SearchRes = Invoke-RestMethod -Uri $Uri -Method Post -Body $JsonBody -Headers @{"content-type"="application/json";"user-persona"="live"} -TimeoutSec 15
        
        if ($SearchRes.data.count -gt 0) {
            $ProdID = $SearchRes.data.fieldValues.uuid[0]
            $EscapedVer = [uri]::EscapeDataString("ESXi $Ver")
            $DetailUri = "https://compatibilityguide.broadcom.com/compguide/programs/viewDetails?programId=io&id=$ProdID&filterBy=$EscapedVer"
            
            $Details = Invoke-RestMethod -Uri $DetailUri -Method Post -Body "" -Headers @{"user-persona"="live"; "accept"="application/json"}
            
            return $Details.data.details.subsections.fieldValues | ForEach-Object {
                [PSCustomObject]@{
                    HostName      = $HostName
                    ESXiVersion   = $Ver
                    Device        = $DeviceName
                    DriverName    = $_.driverName
                    DriverVersion = $_.driverVersion
                    Firmware      = ($_.firmwareVersion + " " + $_.additionalFirmwareVersion).Trim()
                    BCL_Link      = "https://compatibilityguide.broadcom.com/detail.php?deviceid=$ProdID"
                }
            }
        } else {
            return [PSCustomObject]@{
                HostName      = $HostName
                ESXiVersion   = $Ver
                Device        = $DeviceName
                DriverName    = "--- NOT SUPPORTED ---"
                DriverVersion = "N/A"
                Firmware      = "N/A"
                BCL_Link      = "N/A"
            }
        }
    } catch {
        return $null
    }
}

function Get-VCGCPUStatus {
    param([string]$CpuId, [string]$Ver, [string]$HostName, [string]$FullCpuName)
    
    $DetailUri = "https://compatibilityguide.broadcom.com/compguide/programs/viewDetails?programId=cpu&id=$CpuId"
    
    try {
        $Details = Invoke-RestMethod -Uri $DetailUri -Method Post -Body "" -Headers @{"user-persona"="live"; "accept"="application/json"}
        
        $SupportedReleaseField = $Details.data.details.subsections | Where-Object { $_.order -eq 2 } | ForEach-Object { $_.fieldValues }
        
        if (-not $SupportedReleaseField) {
             $SupportedReleaseField = $Details.data.details.subsections.fields | Where-Object { $_.displayKey -eq "esxi" } | Select-Object -ExpandProperty fieldValues
        }

        $IsSupported = $SupportedReleaseField -match $Ver
        $Status = if ($IsSupported) { "SUPPORTED" } else { "--- NOT SUPPORTED ---" }

        return [PSCustomObject]@{
            HostName      = $HostName
            ESXiVersion   = $Ver
            Device        = "CPU: $FullCpuName"
            DriverName    = $Status
            DriverVersion = "N/A"
            Firmware      = "N/A"
            BCL_Link      = "https://compatibilityguide.broadcom.com/detail?program=cpu&productId=$CpuId&persona=live"
        }
    } catch {
        return $null
    }
}

function Get-CpuMatchHints {
    param([string]$FullCpuName)

    $Hints = [ordered]@{
        Vendor           = $null
        ProductLine      = $null
        ModelNumber      = $null
        CodeNames        = @()
        ExpectedPatterns = @()
    }

    if ($FullCpuName -match "\bAMD\b") { $Hints.Vendor = "AMD" }
    elseif ($FullCpuName -match "\bIntel\b") { $Hints.Vendor = "Intel" }

    if ($FullCpuName -match "\bEPYC\b") { $Hints.ProductLine = "EPYC" }
    elseif ($FullCpuName -match "\bXeon\b") { $Hints.ProductLine = "Xeon" }

    if ($FullCpuName -match "EPYC\s+([A-Z0-9]+)") {
        $Hints.ModelNumber = $Matches[1]
        if ($Hints.ModelNumber -match "(\d{4})") {
            $ModelDigits = $Matches[1]
            $FamilyDigit = $ModelDigits.Substring(0, 1)
            $GenerationDigit = $ModelDigits.Substring(3, 1)

            if ($FamilyDigit -in @("7", "8", "9")) {
                $SeriesNumber = "$($FamilyDigit)00$GenerationDigit"
                $Hints.ExpectedPatterns += @(
                    "AMD EPYC $SeriesNumber",
                    "EPYC $SeriesNumber",
                    $SeriesNumber
                )
            }
        }
    }
    elseif ($FullCpuName -match "Xeon.*?\b([0-9]{4,5}[A-Z]*)\b") {
        $Hints.ModelNumber = $Matches[1]
        if ($Hints.ModelNumber -match "^(\d{4,5})([A-Z+]*)$") {
            $ModelDigits = $Matches[1]
            $ModelSuffix = $Matches[2]

            if ($ModelDigits.Length -ge 2) {
                $SkuPrefix = $ModelDigits.Substring(0, 2)

                switch -Regex ($SkuPrefix) {
                    "^(85|65|55)$" {
                        $Hints.CodeNames += "Emerald Rapids-SP"
                        $Hints.ExpectedPatterns += @("8500/6500/5500", "6500/5500", "Emerald-Rapids-SP", "Emerald Rapids")
                    }
                    "^(84|64|54|44|34)$" {
                        $Hints.CodeNames += "Sapphire Rapids-SP"
                        $Hints.ExpectedPatterns += @("8400/6400/5400", "6400/5400", "Sapphire-Rapids-SP", "Sapphire Rapids")
                    }
                    "^(83|63|53|43)$" {
                        if ($ModelSuffix -match "H") {
                            $Hints.CodeNames += "Cooper Lake-SP"
                            $Hints.ExpectedPatterns += @("8300/6300/5300", "6300/5300", "Cooper-Lake-SP", "Cooper Lake")
                        }
                        else {
                            $Hints.CodeNames += "Ice Lake-SP"
                            $Hints.ExpectedPatterns += @("8300/6300/5300", "6300/5300", "Ice-Lake-SP", "Ice Lake")
                        }
                    }
                    "^(62|52|42|32|82)$" {
                        $Hints.CodeNames += "Cascade Lake-SP"
                        $Hints.ExpectedPatterns += @("8200/6200/5200", "6200/5200", "Cascade-Lake-SP", "Cascade Lake")
                    }
                    "^(61|51|41|31|81)$" {
                        $Hints.CodeNames += "Skylake-SP"
                        $Hints.ExpectedPatterns += @("8100/6100/5100", "6100/5100", "Skylake-SP", "Skylake")
                    }
                }
            }
        }
    }

    return [PSCustomObject]$Hints
}

function Get-ScoredCpuCandidates {
    param($CPUList, [string]$FullCpuName)

    $Hints = Get-CpuMatchHints -FullCpuName $FullCpuName
    $SearchTerm = ($FullCpuName -replace "\(R\)|\(TM\)|CPU|@.*$|Processor|Intel|AMD", "").Trim()
    $Words = $SearchTerm -split "\s+" | Where-Object { $_.Length -gt 2 -and $_ -notin @("EPYC", "Xeon", "Core") }

    $ScoredMatches = foreach ($Series in $CPUList) {
        $SeriesName = $Series.cpuSeries.name
        $Score = 0
        $Reasons = @()

        if ($Hints.Vendor -and $SeriesName -like "*$($Hints.Vendor)*") {
            $Score += 20
            $Reasons += "vendor:$($Hints.Vendor)"
        }

        if ($Hints.ProductLine -and $SeriesName -like "*$($Hints.ProductLine)*") {
            $Score += 25
            $Reasons += "line:$($Hints.ProductLine)"
        }

        foreach ($Pattern in $Hints.ExpectedPatterns) {
            if ($SeriesName -like "*$Pattern*") {
                $Score += if ($Pattern -match "Lake|Rapids|Skylake") { 150 } else { 100 }
                $Reasons += "pattern:$Pattern"
            }
        }

        if ($Hints.ModelNumber -and $SeriesName -like "*$($Hints.ModelNumber)*") {
            $Score += 75
            $Reasons += "model:$($Hints.ModelNumber)"
        }

        foreach ($Word in $Words) {
            if ($SeriesName -like "*$Word*") {
                $Score += if ($Word -match "\d") { 40 } else { 5 }
                $Reasons += "word:$Word"
            }
        }

        if ($Score -gt 0) {
            [PSCustomObject]@{
                Score     = $Score
                CpuSeries = $SeriesName
                UUID      = $Series.uuid
                Reasons   = $Reasons -join ", "
                RawObject = $Series
            }
        }
    }

    return $ScoredMatches
}

function Find-VCGCPU {
    param($CPUList, [string]$FullCpuName)

    $BestMatch = Get-ScoredCpuCandidates -CPUList $CPUList -FullCpuName $FullCpuName |
        Sort-Object Score -Descending |
        Select-Object -First 1

    if ($BestMatch -and $BestMatch.Score -ge 75) {
        return $BestMatch.RawObject
    }

    return $null
}

function Export-CompatibilityReport {
    param([System.Collections.ArrayList]$ReportData, [string]$ClusterName)
    $Timestamp = Get-Date -Format "yyyyMMdd_HHmm"
    $FileName = "ESXi_Upgrade_Check_$($ClusterName)_$($Timestamp).csv"
    $ReportData | Export-Csv -Path $FileName -NoTypeInformation -Delimiter ";" -Encoding UTF8
    Write-Host "`n[OK] Rapport opgeslagen: $FileName" -ForegroundColor Green
}

# --- AANGEPASTE OVERLAP LOGICA VOOR CPU ---
function Export-FirmwareOverlapReport {
    param($ReportData, [string]$ClusterName, [string]$TargetVersion)
    
    $OverlapResults = $ReportData | Group-Object HostName, Device | ForEach-Object {
        $DeviceEntries = $_.Group
        $DeviceName = $DeviceEntries[0].Device
        
        # Check of dit een CPU entry is
        if ($DeviceName -like "CPU:*") {
            $TargetStatus = $DeviceEntries | Where-Object { $_.ESXiVersion -eq $TargetVersion } | Select-Object -ExpandProperty DriverName
            $IsSupported = ($TargetStatus -eq "SUPPORTED")

            [PSCustomObject]@{
                HostName       = $DeviceEntries[0].HostName
                Device         = $DeviceName
                CommonFirmware = "N/A (CPU Check)"
                UpgradeNeeded  = if ($IsSupported) { "No - CPU is Supported" } else { "YES - CPU NOT SUPPORTED FOR $TargetVersion" }
                BCL_Link       = $DeviceEntries[0].BCL_Link
            }
        }
        else {
            # Originele Hardware Logica
            $FW_v8 = $DeviceEntries | Where-Object { $_.ESXiVersion -eq "8.0 U3" } | ForEach-Object { $_.Firmware -split " " } | Select-Object -Unique
            $FW_v9 = $DeviceEntries | Where-Object { $_.ESXiVersion -eq $TargetVersion } | ForEach-Object { $_.Firmware -split " " } | Select-Object -Unique
            $CommonFirmware = $FW_v8 | Where-Object { $FW_v9 -contains $_ -and $_ -ne "N/A" }

            [PSCustomObject]@{
                HostName       = $DeviceEntries[0].HostName
                Device         = $DeviceName
                CommonFirmware = if ($CommonFirmware) { $CommonFirmware -join ", " } else { "NO OVERLAP FOUND" }
                UpgradeNeeded  = if ($CommonFirmware) { "No - Common FW available" } else { "Yes - Firmware change required" }
                BCL_Link       = $DeviceEntries[0].BCL_Link
            }
        }
    }

    $Timestamp = Get-Date -Format "yyyyMMdd_HHmm"
    $FileName = "Firmware_Upgrade_Strategy_$($ClusterName)_$($Timestamp).csv"
    $OverlapResults | Export-Csv -Path $FileName -NoTypeInformation -Delimiter ";" -Encoding UTF8
    Write-Host "[OK] Strategie rapport opgeslagen: $FileName" -ForegroundColor Cyan
}

# --- MAIN EXECUTION ---
Connect-VIServer -Server $vCenterName

Write-Host "Ophalen VCG CPU Masterlijst..." -ForegroundColor Gray
$CpuBodyObj = @{ programId = "cpu"; filters = @(); keyword = @(); date = @{ startDate = $null; endDate = $null } } | ConvertTo-Json

try {
    $GlobalCPUList = (Invoke-RestMethod -Uri "https://compatibilityguide.broadcom.com/compguide/programs/viewResults?limit=500&page=1" -Method Post -Body $CpuBodyObj -Headers @{"content-type"="application/json";"user-persona"="live"}).data.fieldValues
} catch {
    Write-Error "Kon CPU Masterlijst niet ophalen."
}

$Clusters = Get-Cluster
foreach ($cluster in $Clusters) {
    $ClusterName = $cluster.Name
    $TargetVersions = @("8.0 U3", $TargetVersion)
    $Hosts = Get-ClusterHosts -ClusterName $ClusterName

    if ($Hosts) {
        $FullReport = foreach ($ESXiHost in $Hosts) {
            Write-Host "`n>>> Processing Host: $($ESXiHost.Name)" -ForegroundColor Cyan
            
            # --- CPU CHECK ---
            $FullCpuName = $ESXiHost.ExtensionData.Summary.Hardware.CpuModel
            $SearchTerm = ($FullCpuName -replace "\(R\)|\(TM\)|CPU|@.*$|Processor|Intel|AMD", "").Trim()
            
            $MatchedCpu = $null
            if ($GlobalCPUList) {
                $MatchedCpu = Find-VCGCPU -CPUList $GlobalCPUList -FullCpuName $FullCpuName
            }

            if ($MatchedCpu) {
                Write-Host "    VCG CPU Series match: $($MatchedCpu.cpuSeries.name)" -ForegroundColor Gray
            }
            else {
                $CpuHints = Get-CpuMatchHints -FullCpuName $FullCpuName
                if ($CpuHints.ExpectedPatterns.Count -gt 0) {
                    Write-Warning "Geen passende VCG CPU Series gevonden voor CPU: $FullCpuName. Geprobeerde hints: $($CpuHints.ExpectedPatterns -join ', ')"
                }
                else {
                    Write-Warning "Geen passende VCG CPU Series gevonden voor CPU: $FullCpuName"
                }
            }

            foreach ($Ver in $TargetVersions) {
                if ($MatchedCpu) {
                    Write-Host "    Checking $Ver for CPU: $SearchTerm..." -NoNewline
                    $CpuRes = Get-VCGCPUStatus -CpuId $MatchedCpu.uuid -Ver $Ver -HostName $ESXiHost.Name -FullCpuName $FullCpuName
                    if ($CpuRes) {
                        $StatusColor = if ($CpuRes.DriverName -eq "SUPPORTED") { "Green" } else { "Red" }
                        Write-Host " [$($CpuRes.DriverName)]" -ForegroundColor $StatusColor
                        $CpuRes
                    }
                }
            }

            # --- HARDWARE CHECK ---
            $esxcli = Get-EsxCli -VMHost $ESXiHost -V2
            $HardwarePCIList = $esxcli.hardware.pci.list.Invoke()
            $UniqueHardwareList = $HardwarePCIList | Where-Object {
                $_.DeviceClassName -match "Network|Ethernet|Storage|RAID|SCSI|Fibre Channel" -and
                $_.DeviceClassName -notmatch "Bridge|Peripheral|Hub"
            } | Group-Object VendorID, DeviceID, SubVendorID, SubDeviceID | ForEach-Object { $_.Group[0] }

            foreach ($Hw in $UniqueHardwareList) {
                $VID  = ("{0:X4}" -f [int]$Hw.VendorID).ToLower()
                $DID  = ("{0:X4}" -f [int]$Hw.DeviceID).ToLower()
                $SVID = ("{0:X4}" -f [int]$Hw.SubVendorID).ToLower()
                $SDID = ("{0:X4}" -f [int]$Hw.SubDeviceID).ToLower()

                foreach ($Ver in $TargetVersions) {
                    Write-Host "    Checking $Ver for $($Hw.DeviceName)..." -NoNewline
                    $Res = Get-VCGComparison -VID $VID -DID $DID -SVID $SVID -SDID $SDID -DeviceName $Hw.DeviceName -Ver $Ver -HostName $ESXiHost.Name
                    if ($Res) { Write-Host " [OK]" -ForegroundColor Green; $Res } else { Write-Host " [NO DATA]" -ForegroundColor Yellow }
                }
            }
        }

        if ($FullReport) {
            Export-CompatibilityReport -ReportData $FullReport -ClusterName $ClusterName
            Export-FirmwareOverlapReport -ReportData $FullReport -ClusterName $ClusterName -TargetVersion $TargetVersion
        }
    }
}
Disconnect-VIServer -Server * -Confirm:$false 

 

Leave a Reply

Your email address will not be published. Required fields are marked *