Private/Checks/Esx/Test-VcfEsxHardwareDetails.ps1

# Copyright (c) 2026 Broadcom. All Rights Reserved.
# Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
#
# =============================================================================
#
# SOFTWARE LICENSE AGREEMENT
#
# Copyright (c) CA, Inc. All rights reserved.
#
# You are hereby granted a non-exclusive, worldwide, royalty-free license
# under CA, Inc.'s copyrights to use, copy, modify, and distribute this
# software in source code or binary form for use in connection with CA, Inc.
# products.
#
# This copyright notice shall be included in all copies or substantial
# portions of the software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
# =============================================================================
function Test-VcfEsxHardwareDetails {

    <#
        .SYNOPSIS
        Reports hardware details (server, CPU, boot mode, network adapters, storage adapters, SCSI devices)
        for every ESX host across all vCenters managed by SDDC Manager.

        .DESCRIPTION
        Gathers hardware inventory (BIOS version, vendor, model, boot mode, CPU, network/storage
        adapters, and SCSI devices) for every ESX host on each connected vCenter.

        Reports 'Fail' for a vCenter domain if any host's CPU is not found on the shipped
        Broadcom Compatibility Guide CPU series snapshot for EsxDestinationVersion (see
        Resolve-VcfCheckEsxCpuCompatibility) - a blocking condition, since an unlisted CPU is
        likely unsupported on the target ESX release - or if any host's CPU is Discontinued per
        Broadcom KB 318697 (see Resolve-VcfCheckEsxCpuDeprecationStatus). Reports 'Warning'
        (non-blocking) if any host's CPU is Deprecated per that same KB - support for it is
        expected to be removed in a future major release. Reports 'Pass' if every host's CPU is
        confirmed compatible and not on the deprecation/discontinuation track, or could not be
        evaluated, or 'Error' if no hosts are found, a vCenter connection fails, or an execution
        error occurs.

        If $Context.ReportDirectoryPath is set, exports a timestamped JSON report ('esx-hardware-report-<timestamp>.json')
        per vCenter domain containing the host hardware findings for offline analysis. Results are returned
        per domain using New-VcfCheckPerDomainResults.

        Gathers detailed adapter information per host via Get-EsxCli calls. DEBUG-level Write-LogMessage
        calls log the host inventory fetch and individual host collection times to the log file for
        diagnosing execution performance without cluttering console output.

        Each host's CPU is flagged against the shipped Broadcom Compatibility Guide CPU series
        snapshot, entirely offline. Hosts whose CPU is not found on that list, or could not be
        evaluated, are sorted to the top of each domain's host list so they are seen first.

        .PARAMETER Context
        The VcfCheck.Context object. Must be connected to SDDC Manager. May contain ReportDirectoryPath
        for JSON report export.

        .PARAMETER DisplayName
        Optional display name for the check. If omitted, defaults to the display name specified in the
        precheck catalog entry for 'esx_hardware_details'.

        .PARAMETER EsxDestinationVersion
        The ESX release family to flag CPU compatibility against, e.g. '9.0' or '9.1'. Defaults to
        '9.1', the newest release the shipped CPU compatibility snapshot covers.

        .OUTPUTS
        [PSObject[]] Array of per-domain check result objects created by New-VcfCheckPerDomainResults.
    #>


    [CmdletBinding()]
    [OutputType([PSObject[]])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Context,
        [Parameter(Mandatory = $false)] [String]$DisplayName = '',
        [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$EsxDestinationVersion = '9.1'
    )

    $startedAt = Get-Date
    $checkId = 'esx_hardware_details'
    $catalogEntry = (Get-VcfCheckCatalog)[$checkId]
    $displayName = if ([String]::IsNullOrEmpty($DisplayName)) { $catalogEntry.displayName } else { $DisplayName }
    $validationCriteria = $catalogEntry.validationCriteria

    try {
        $vcenterFqdns = Get-VcfCheckAllVCenterFqdns -Context $Context
    } catch {
        return New-VcfCheckResult -CheckId $checkId -Area ESX -Status Error `
            -Exception $_.Exception.Message -ValidationCriteria $validationCriteria -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $displayName
    }

    $outcomes = foreach ($vcenterFqdn in $vcenterFqdns) {
        $iterationStartedAt = Get-Date
        $outcome = try {
            Connect-VcfCheckVCenter -Context $Context -Fqdn $vcenterFqdn
            $hostInventoryStartedAt = Get-Date
            $hosts = @(Get-VcfCheckVMHostInventory -Server $vcenterFqdn)
            $hostInventoryMs = ((Get-Date) - $hostInventoryStartedAt).TotalMilliseconds
            Write-LogMessage -Type DEBUG -Message "[$vcenterFqdn] Host inventory returned $($hosts.Count) host(s) in $(Format-VcfCheckDuration -Milliseconds $hostInventoryMs)."

            if ($hosts.Count -eq 0) {
                [PSCustomObject]@{ VCenterFqdn = $vcenterFqdn; Status = 'Error'; Detail = 'No ESX hosts found.'; HostDetails = @(); HostDetailsLabel = 'Summary' }
            } else {
                $hostIndex = 0
                $hostDetails = @($hosts | ForEach-Object {
                    $hostIndex++
                    Write-VcfCheckSubProgress -Context $Context -Current $hostIndex -Total $hosts.Count -Label $_.Name
                    $hostStartedAt = Get-Date
                    $hostDetail = Get-VcfCheckEsxHostHardwareDetail -VMHost $_ -EsxDestinationVersion $EsxDestinationVersion
                    $hostMs = ((Get-Date) - $hostStartedAt).TotalMilliseconds
                    Write-LogMessage -Type DEBUG -Message "[$vcenterFqdn] Host `"$($_.Name)`" hardware detail collected in $(Format-VcfCheckDuration -Milliseconds $hostMs)."
                    $hostDetail
                })

                $reportPath = $null
                if ($Context.ReportDirectoryPath) {
                    $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
                    $reportPath = Join-Path -Path $Context.ReportDirectoryPath -ChildPath "esx-hardware-report-$timestamp.json"
                    $report = @{
                        GeneratedAt = Get-Date -Format 'o'
                        VCenter     = $vcenterFqdn
                        Hosts       = $hostDetails
                    }
                    $json = $null
                    foreach ($depth in @(5, 3, 2)) {
                        try {
                            $json = $report | ConvertTo-Json -Depth $depth -ErrorAction Stop
                            break
                        }
                        catch {
                            if ($depth -eq 2) {
                                throw $_
                            }
                        }
                    }
                    $json | Set-Content -LiteralPath $reportPath -Encoding utf8
                }

                $detail = "Successfully retrieved hardware details for $($hostDetails.Count) host$(if ($hostDetails.Count -ne 1) { 's' })."
                if ($reportPath) { $detail += " Detailed hardware report: $reportPath" }

                $cpuIssueSortRank = @{ NotListed = 0; Discontinued = 0; Deprecated = 1; Unknown = 2; Compatible = 3; None = 3 }
                $sortedHostDetails = @($hostDetails | Sort-Object -Property `
                    @{ Expression = { [Math]::Min($cpuIssueSortRank[$_.CpuCompatibility], $cpuIssueSortRank[$_.CpuDeprecationStatus]) } }, HostName)

                $notListedHosts = @($sortedHostDetails | Where-Object { $_.CpuCompatibility -eq 'NotListed' })
                $discontinuedHosts = @($sortedHostDetails | Where-Object { $_.CpuDeprecationStatus -eq 'Discontinued' })
                $deprecatedHosts = @($sortedHostDetails | Where-Object { $_.CpuDeprecationStatus -eq 'Deprecated' })

                $status = 'Pass'
                if ($notListedHosts.Count -gt 0) {
                    $status = 'Fail'
                    $detail += " $($notListedHosts.Count) host$(if ($notListedHosts.Count -ne 1) { 's' }) with a CPU not found on Broadcom's Compatibility Guide for ESX $EsxDestinationVersion`: $(($notListedHosts | ForEach-Object { $_.HostName }) -join ', ')."
                }
                if ($discontinuedHosts.Count -gt 0) {
                    $status = 'Fail'
                    $detail += " $($discontinuedHosts.Count) host$(if ($discontinuedHosts.Count -ne 1) { 's' }) with a CPU discontinued for ESX $EsxDestinationVersion`: $(($discontinuedHosts | ForEach-Object { $_.HostName }) -join ', ')."
                }
                if ($deprecatedHosts.Count -gt 0) {
                    if ($status -eq 'Pass') { $status = 'Warning' }
                    $detail += " $($deprecatedHosts.Count) host$(if ($deprecatedHosts.Count -ne 1) { 's' }) with a CPU deprecated in VCF 9.0 with removal planned for a future version of VCF: $(($deprecatedHosts | ForEach-Object { $_.HostName }) -join ', '). See https://knowledge.broadcom.com/external/article/318697 for details."
                }

                [PSCustomObject]@{ VCenterFqdn = $vcenterFqdn; Status = $status; Detail = $detail; HostDetails = $sortedHostDetails; HostDetailsLabel = 'Summary' }
            }
        } catch {
            [PSCustomObject]@{ VCenterFqdn = $vcenterFqdn; Status = 'Error'; Detail = $_.Exception.Message; HostDetails = @(); HostDetailsLabel = 'Summary' }
        }
        $outcome | Add-Member -NotePropertyName StartedAt -NotePropertyValue $iterationStartedAt -Force
        $outcome | Add-Member -NotePropertyName CompletedAt -NotePropertyValue (Get-Date) -Force
        $outcome
    }

    return New-VcfCheckPerDomainResults -Context $Context -PerVCenterOutcome $outcomes -CheckId $checkId `
        -StartedAt $startedAt
}
function Get-VcfCheckEsxHostHardwareDetail {

    <#
        .SYNOPSIS
        Collects server, cluster, CPU, boot mode, network adapter, storage adapter, and storage device details for a single ESX host.

        .DESCRIPTION
        Helper cmdlet that extracts detailed hardware configuration for an ESX host object retrieved from Get-VcfCheckVMHostInventory.

        Queries and computes:
        - Host name, cluster name, vendor, model, and BIOS version
        - Boot mode (resolving firmware type, UEFI/Secure Boot capability, or Legacy BIOS)
        - CPU series and total core count via Get-VcfCheckVMHostCpuInfo
        - Physical network adapters via Get-VcfCheckVMHostNetworkDevices
        - Storage adapters via Get-VcfCheckVMHostStorageAdapters
        - SCSI storage devices and capacities via Get-VcfCheckVMHostScsiDevices
        - CPU compatibility flag against the shipped Broadcom Compatibility Guide CPU series
          snapshot for EsxDestinationVersion, via Resolve-VcfCheckEsxCpuCompatibility
        - CPU deprecation/discontinuation flag against Broadcom KB 318697's CPU support removal
          track for EsxDestinationVersion, via Resolve-VcfCheckEsxCpuDeprecationStatus - a CPU
          can be Compatible on the Hardware Compatibility Guide and still be Deprecated or
          Discontinued here, since the two are independent Broadcom data sources

        .PARAMETER VMHost
        A VMHost inventory object retrieved via Get-VcfCheckVMHostInventory.

        .PARAMETER EsxDestinationVersion
        The ESX release family to flag CPU compatibility against, e.g. '9.0' or '9.1'.

        .OUTPUTS
        [PSCustomObject] Structured object containing fields: HostName, ClusterName, Vendor, Model,
        BiosVersion, BootMode, CpuSeries, CpuCores, CpuCompatibility, CpuCompatibilityMatchedSeries,
        CpuDeprecationStatus, CpuDeprecationMatchedSeries, NetworkAdapters, StorageAdapters, and
        StorageDevices.
    #>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$VMHost,
        [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$EsxDestinationVersion = '9.1'
    )

    $bios = $VMHost.ExtensionData.Hardware.BiosInfo
    $cpuInfo = Get-VcfCheckVMHostCpuInfo -VMHost $VMHost -ErrorAction SilentlyContinue
    $cpuCompatibility = Resolve-VcfCheckEsxCpuCompatibility -CpuModel $cpuInfo.CpuSeries -EsxVersion $EsxDestinationVersion
    $cpuDeprecation = Resolve-VcfCheckEsxCpuDeprecationStatus -CpuModel $cpuInfo.CpuSeries -EsxVersion $EsxDestinationVersion

    $cluster = $null
    try {
        if ($VMHost.Parent -and $VMHost.Parent.Name) {
            $cluster = $VMHost.Parent.Name
        } elseif ($VMHost.ExtensionData.Parent -and $VMHost.ExtensionData.Parent.Name) {
            $cluster = $VMHost.ExtensionData.Parent.Name
        }
    } catch {
        $cluster = $null
    }

    $bootMode = 'Unknown'
    try {
        $firmwareType = $VMHost.ExtensionData.Hardware.BiosInfo.FirmwareType
        if ([string]::IsNullOrWhiteSpace($firmwareType)) {
            if ($VMHost.ExtensionData.Capability.SecureBootSupported -eq $true) {
                $bootMode = 'UEFI (SecureBoot Capable)'
            } else {
                $bootMode = 'Legacy BIOS'
            }
        } else {
            $bootMode = $firmwareType
        }
    } catch {
        $bootMode = 'Unknown'
    }

    $networkStartedAt = Get-Date
    $networkAdapters = @(Get-VcfCheckVMHostNetworkDevices -VMHost $VMHost -ErrorAction SilentlyContinue | ForEach-Object {
        [PSCustomObject]@{ Name = $_.Name; Vendor = $_.Vendor; Model = $_.Model }
    })
    $networkMs = ((Get-Date) - $networkStartedAt).TotalMilliseconds

    $storageAdaptersStartedAt = Get-Date
    $storageAdapters = @(Get-VcfCheckVMHostStorageAdapters -VMHost $VMHost -ErrorAction SilentlyContinue | ForEach-Object {
        [PSCustomObject]@{ Name = $_.Name; Vendor = $_.Vendor; Model = $_.Model; Type = $_.Type }
    })
    $storageAdaptersMs = ((Get-Date) - $storageAdaptersStartedAt).TotalMilliseconds

    $scsiStartedAt = Get-Date
    $storageDevices = @(Get-VcfCheckVMHostScsiDevices -VMHost $VMHost -ErrorAction SilentlyContinue | ForEach-Object {
        [PSCustomObject]@{ Name = $_.Name; Vendor = $_.Vendor; Model = $_.Model; Type = $_.Type; CapacityGB = $_.Capacity }
    })
    $scsiMs = ((Get-Date) - $scsiStartedAt).TotalMilliseconds

    Write-LogMessage -Type DEBUG -Message "[$($VMHost.Name)] NICs: $(Format-VcfCheckDuration -Milliseconds $networkMs) ($($networkAdapters.Count)), HBAs: $(Format-VcfCheckDuration -Milliseconds $storageAdaptersMs) ($($storageAdapters.Count)), SCSI devices: $(Format-VcfCheckDuration -Milliseconds $scsiMs) ($($storageDevices.Count))."

    return [PSCustomObject]@{
        HostName                      = $VMHost.Name
        ClusterName                   = $cluster
        Vendor                        = $VMHost.ExtensionData.Summary.Hardware.Vendor
        Model                         = $VMHost.ExtensionData.Summary.Hardware.Model
        BiosVersion                   = $bios.BiosVersion
        BootMode                      = $bootMode
        CpuSeries                     = $cpuInfo.CpuSeries
        CpuCores                      = $cpuInfo.TotalCores
        CpuCompatibility              = $cpuCompatibility.Status
        CpuCompatibilityMatchedSeries = $cpuCompatibility.MatchedSeries
        CpuDeprecationStatus          = $cpuDeprecation.Status
        CpuDeprecationMatchedSeries   = $cpuDeprecation.MatchedSeries
        NetworkAdapters               = $networkAdapters
        StorageAdapters               = $storageAdapters
        StorageDevices                = $storageDevices
    }
}