Private/Checks/Vsan/Test-VcfVsanHclCompliance.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-VcfVsanHclCompliance { <# .SYNOPSIS Flags any ESX host storage controller or NIC that is not on Broadcom's published vSAN Hardware Compatibility List (HCL) for the target VCF upgrade release. .DESCRIPTION For every ESX host across all vCenters managed by SDDC Manager, matches each storage controller's and NIC's PCI VendorId/DeviceId/SubVendorId/SubDeviceId against the shipped vSAN HCL snapshot (see Resolve-VcfCheckVsanHclCompatibility), and each SSD/HDD drive against the shipped drive HCL snapshot by PCI identity when available or reported model string otherwise (see Resolve-VcfCheckVsanHclDriveCompatibility and Get-VcfCheckVsanHclDriveHbaPciIdentity - only NVMe drives carry a PCI identity of their own, since an NVMe drive is its own PCIe endpoint; SAS/SATA drives sit behind a shared HBA and are matched on model string alone), for the target ESX release being upgraded to (VcfDestinationRelease), not the host's currently-installed release - this is a pre-upgrade readiness check, and the shipped HCL snapshots only carry data for ESXi 9.0/9.1, so resolving against an older installed release would report every component as unconfirmed. NICs additionally match on driver name/version/firmware, since that data is available for NICs but not for storage adapters (see Get-VcfCheckVMHostStorageAdapters). Reports 'Fail' for a vCenter domain if any host has a component currently in use by vSAN (CurrentlyUsedByvSAN) whose PCI identity is not found anywhere in the shipped HCL data ('NotListed'), whose driver/firmware combination does not appear on the HCL for an otherwise-matched component ('FirmwareUnsupported'), or that is certified on the HCL only for an ESX release older than the target upgrade release ('IncompatibleWithTargetRelease' - see Resolve-VcfCheckVsanHclOlderFamily). A component not currently backing vSAN can't cause a vSAN upgrade problem, so its status never affects the check's outcome - it's still reported (see Get-VcfCheckVsanHclHostDetail's Components/CurrentlyUsedByvSAN), just not counted towards NotListedComponents/FirmwareUnsupportedComponents/IncompatibleWithTargetReleaseComponents. Components that could not be evaluated at all (no shipped HCL data for the target or any older ESX release, or a missing PCI identity) are treated as 'Unknown' and do not affect the check's status, mirroring how Test-VcfEsxHardwareDetails treats an unevaluable CPU. Reports 'Pass' if every in-use component is confirmed compatible or could not be evaluated, or 'Error' if no hosts are found, a vCenter connection fails, or an execution error occurs. Reuses the per-run device cache shared with Test-VcfEsxHardwareDetails (Get-VcfCheckCachedHostHardwareDetail) so both checks never pay for the expensive per-host esxcli collection independently. .PARAMETER Context The VcfCheck.Context object. Must be connected to SDDC Manager. .PARAMETER VcfDestinationRelease Same top-level destination-release selection Test-VcfSddcBomCheck accepts - a full concrete release, a release family (major.minor.patch), or 'latest'. A family or 'latest' is resolved to ESX's own newest matching release via Resolve-VcfCheckInteropMatrixReleaseInFamily, and only that release's major.minor family (e.g. '9.1') is used to select the shipped HCL data. Defaults to $Script:VcfCheckDefaultDestinationRelease ('latest'). .PARAMETER DisplayName Optional display name for the check. If omitted, defaults to the display name specified in the precheck catalog entry for 'vsan_hcl_compliance'. .PARAMETER MaxConcurrentHosts Maximum number of hosts per vCenter collected concurrently (ForEach-Object -Parallel - each host's esxcli/PowerCLI collection is an independent per-host round trip, so this is the only cross-host resource contention). Defaults to 10. A single host's own connection to vCenter is established inside its own runspace via Connect-VcfCheckVCenter, since PowerCLI's ambient $global:DefaultVIServers session state does not cross runspace boundaries. A value of 1 collects hosts sequentially in the caller's own runspace instead - Pester's Mock cannot see into a separate -Parallel runspace (each one re-imports the module fresh), so tests exercising per-host mock behavior pass -MaxConcurrentHosts 1. Write-VcfCheckSubProgress's Current/Total still count accurately with concurrent collection (a synchronized counter incremented once per completed host), but Label names whichever host most recently finished, not a batch - up to MaxConcurrentHosts hosts are being collected at any moment, so the browser UI's "Scanning N/Total hosts (<label>)" line advances in completion order, not host-list order. .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)] [ValidateNotNullOrEmpty()] [String]$VcfDestinationRelease = $Script:VcfCheckDefaultDestinationRelease, [Parameter(Mandatory = $false)] [String]$DisplayName = '', [Parameter(Mandatory = $false)] [ValidateRange(1, 100)] [Int]$MaxConcurrentHosts = 10 ) $startedAt = Get-Date $checkId = 'vsan_hcl_compliance' $catalogEntry = (Get-VcfCheckCatalog)[$checkId] $displayName = if ([String]::IsNullOrEmpty($DisplayName)) { $catalogEntry.displayName } else { $DisplayName } $validationCriteria = $catalogEntry.validationCriteria $targetEsxVersion = $VcfDestinationRelease $isDestinationReleaseFamily = ($VcfDestinationRelease -eq 'latest') -or ($VcfDestinationRelease -match '^\d+\.\d+\.\d+$') if ($isDestinationReleaseFamily) { $resolvedRelease = Resolve-VcfCheckInteropMatrixReleaseInFamily -Component 'ESX' -Family $VcfDestinationRelease if ($resolvedRelease) { $targetEsxVersion = $resolvedRelease } } try { $vcenterFqdns = Get-VcfCheckAllVCenterFqdns -Context $Context } catch { return New-VcfCheckResult -CheckId $checkId -Area vSAN -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 $hosts = @(Get-VcfCheckVMHostInventory -Server $vcenterFqdn) if ($hosts.Count -eq 0) { [PSCustomObject]@{ VCenterFqdn = $vcenterFqdn; Status = 'Error'; Detail = 'No ESX hosts found.'; HostDetails = @(); HostDetailsLabel = 'Hosts' } } else { if ($MaxConcurrentHosts -le 1) { $hostIndex = 0 $hostDetails = @($hosts | ForEach-Object { $hostIndex++ Write-VcfCheckSubProgress -Context $Context -Current $hostIndex -Total $hosts.Count -Label $_.Name Get-VcfCheckVsanHclHostDetail -VMHost $_ -Context $Context -EsxVersion $targetEsxVersion }) } else { $vcfCheckModulePath = (Get-Module -Name VcfCheck).Path $progressCounter = [Hashtable]::Synchronized(@{ Current = 0 }) $totalHostCount = $hosts.Count $hostDetails = @($hosts | ForEach-Object -ThrottleLimit $MaxConcurrentHosts -Parallel { $vmHost = $_ $context = $using:Context $esxVersion = $using:targetEsxVersion $fqdn = $using:vcenterFqdn $counter = $using:progressCounter $totalHosts = $using:totalHostCount Import-Module -Name $using:vcfCheckModulePath -Force Connect-VcfCheckVCenter -Context $context -Fqdn $fqdn $detail = Get-VcfCheckVsanHclHostDetail -VMHost $vmHost -Context $context -EsxVersion $esxVersion [System.Threading.Monitor]::Enter($counter) try { $counter.Current++ Write-VcfCheckSubProgress -Context $context -Current $counter.Current -Total $totalHosts -Label $vmHost.Name } finally { [System.Threading.Monitor]::Exit($counter) } $detail }) } $sortedHostDetails = @($hostDetails | Sort-Object -Property ` @{ Expression = { if ($_.NotListedComponents.Count -gt 0 -or $_.FirmwareUnsupportedComponents.Count -gt 0 -or $_.IncompatibleWithTargetReleaseComponents.Count -gt 0) { 0 } else { 1 } } }, HostName) $notListedHosts = @($sortedHostDetails | Where-Object { $_.NotListedComponents.Count -gt 0 }) $unsupportedHosts = @($sortedHostDetails | Where-Object { $_.FirmwareUnsupportedComponents.Count -gt 0 }) $incompatibleHosts = @($sortedHostDetails | Where-Object { $_.IncompatibleWithTargetReleaseComponents.Count -gt 0 }) $detail = "Successfully checked vSAN HCL compliance for $($hostDetails.Count) host$(if ($hostDetails.Count -ne 1) { 's' }) against ESX $targetEsxVersion." $status = 'Pass' if ($notListedHosts.Count -gt 0) { $status = 'Fail' $detail += " $($notListedHosts.Count) host$(if ($notListedHosts.Count -ne 1) { 's' }) with a storage controller or NIC not found on the vSAN Hardware Compatibility List: $(($notListedHosts | ForEach-Object { $_.HostName }) -join ', ')." } if ($unsupportedHosts.Count -gt 0) { $status = 'Fail' $detail += " $($unsupportedHosts.Count) host$(if ($unsupportedHosts.Count -ne 1) { 's' }) with a NIC driver/firmware combination not on the vSAN Hardware Compatibility List: $(($unsupportedHosts | ForEach-Object { $_.HostName }) -join ', ')." } if ($incompatibleHosts.Count -gt 0) { $status = 'Fail' $detail += " $($incompatibleHosts.Count) host$(if ($incompatibleHosts.Count -ne 1) { 's' }) with a storage controller, NIC, or drive certified on the vSAN Hardware Compatibility List only for an ESX release older than $targetEsxVersion - verify with the vendor before upgrading: $(($incompatibleHosts | ForEach-Object { $_.HostName }) -join ', ')." } [PSCustomObject]@{ VCenterFqdn = $vcenterFqdn; Status = $status; Detail = $detail; HostDetails = $sortedHostDetails; HostDetailsLabel = 'Hosts' } } } catch { [PSCustomObject]@{ VCenterFqdn = $vcenterFqdn; Status = 'Error'; Detail = $_.Exception.Message; HostDetails = @(); HostDetailsLabel = 'Hosts' } } $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-VcfCheckVsanHclHostDetail { <# .SYNOPSIS Resolves vSAN HCL compliance for a single ESX host's storage controllers and NICs. .DESCRIPTION Reads the host's network/storage adapter PCI identities and drives from the per-run device cache (Get-VcfCheckCachedHostHardwareDetail) and matches each one against the shipped vSAN HCL snapshots via Resolve-VcfCheckVsanHclCompatibility / Resolve-VcfCheckVsanHclDriveCompatibility, using the target upgrade ESX version (EsxVersion) rather than the host's own installed version, since the shipped HCL data only covers the upgrade destination releases. NICs are matched including driver name/version/firmware; storage adapters are matched on PCI identity only, since no firmware version is available for them (see Get-VcfCheckVMHostStorageAdapters). An NVMe drive is its own PCIe endpoint and so is also enumerated as a single-LUN storage adapter - those synthetic entries are excluded from storageComponents (matched via Drive.HbaName) so the drive isn't evaluated twice under two different PCI-derived identities. Drives are matched on PCI identity when the drive is NVMe (see Get-VcfCheckVsanHclDriveHbaPciIdentity) or reported model string otherwise, plus firmware revision. Each component row also carries CurrentlyUsedByvSAN: for drives, resolved against the host's vSAN disk group inventory (see Get-VcfCheckVsanHclHostMemberDiskCanonicalNames); for network adapters and storage controllers, resolved against the host's live vSAN traffic/controller usage (see Get-VcfCheckVsanHclHostInUseDeviceNames). .PARAMETER VMHost A VMHost inventory object retrieved via Get-VcfCheckVMHostInventory. .PARAMETER Context The VcfCheck.Context object, used to read the shared per-host device cache. .PARAMETER EsxVersion The target ESX release being upgraded to (e.g. '9.1.0.0300'), resolved by the caller from VcfDestinationRelease. Only the major.minor family is used to select shipped HCL data. .OUTPUTS [PSCustomObject] with HostName, ClusterName, Components (per-component verdicts, grouped by DeviceType/Vendor/Model/Status via Group-VcfCheckVsanHclComponentsByModel for display - includes hardware not currently used by vSAN), NotListedComponents, FirmwareUnsupportedComponents, and IncompatibleWithTargetReleaseComponents (all three ungrouped, filtered by Status from the per-device verdicts and restricted to CurrentlyUsedByvSAN components only - these three drive the check's Fail status, so hardware not backing vSAN today never fails the check). #> [CmdletBinding()] [OutputType([PSObject])] Param ( [Parameter(Mandatory = $true)] [PSObject]$VMHost, [Parameter(Mandatory = $true)] [PSObject]$Context, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$EsxVersion ) $cachedDeviceDetail = Get-VcfCheckCachedHostHardwareDetail -Context $Context -VMHost $VMHost $vsanMemberDiskNames = @(Get-VcfCheckVsanHclHostMemberDiskCanonicalNames -Context $Context -VMHost $VMHost) $inUseDeviceNames = Get-VcfCheckVsanHclHostInUseDeviceNames -Context $Context -VMHost $VMHost $networkComponents = @($cachedDeviceDetail.NetworkAdapters | ForEach-Object { $verdict = Resolve-VcfCheckVsanHclCompatibility -VendorId $_.VendorId -DeviceId $_.DeviceId ` -SubVendorId $_.SubVendorId -SubDeviceId $_.SubDeviceId -EsxVersion $EsxVersion ` -DriverName $_.Driver -DriverVersion $_.DriverVersion -FirmwareVersion $_.FirmwareVersion [PSCustomObject]@{ Name = $_.Name; DeviceType = 'Network'; Vendor = $_.Vendor; Model = $_.Model; Status = $verdict.Status; SupportedVsanTypes = $verdict.SupportedVsanTypes; LatestCompatibleRelease = $verdict.LatestCompatibleRelease; CurrentlyUsedByvSAN = ($inUseDeviceNames.NetworkAdapterNames -contains $_.Name) } }) $nvmeDriveHbaNames = @($cachedDeviceDetail.Drives | Where-Object { "$($_.Type)" -like 'NVMe *' } | ForEach-Object { $_.HbaName }) $storageComponents = @($cachedDeviceDetail.StorageAdapters | Where-Object { $nvmeDriveHbaNames -notcontains $_.Name } | ForEach-Object { $verdict = Resolve-VcfCheckVsanHclCompatibility -VendorId $_.VendorId -DeviceId $_.DeviceId ` -SubVendorId $_.SubVendorId -SubDeviceId $_.SubDeviceId -EsxVersion $EsxVersion [PSCustomObject]@{ Name = $_.Name; DeviceType = $_.Type; Vendor = $_.Vendor; Model = $_.Model; Status = $verdict.Status; SupportedVsanTypes = $verdict.SupportedVsanTypes; LatestCompatibleRelease = $verdict.LatestCompatibleRelease; CurrentlyUsedByvSAN = ($inUseDeviceNames.ControllerNames -contains $_.Name) } }) $driveComponents = @($cachedDeviceDetail.Drives | ForEach-Object { $pciIdentity = Get-VcfCheckVsanHclDriveHbaPciIdentity -Drive $_ -StorageAdapters $cachedDeviceDetail.StorageAdapters $verdict = Resolve-VcfCheckVsanHclDriveCompatibility -Model $_.Model -Vendor $_.Vendor -EsxVersion $EsxVersion -FirmwareRevision $_.Revision ` -VendorId $pciIdentity.VendorId -DeviceId $pciIdentity.DeviceId -SubVendorId $pciIdentity.SubVendorId -SubDeviceId $pciIdentity.SubDeviceId [PSCustomObject]@{ Name = $_.Name; DeviceType = $_.Type; Vendor = $_.Vendor; Model = $_.Model; Status = $verdict.Status; SupportedVsanTypes = $verdict.SupportedVsanTypes; LatestCompatibleRelease = $verdict.LatestCompatibleRelease; CurrentlyUsedByvSAN = ($vsanMemberDiskNames -contains $_.Name) } }) $components = @($networkComponents) + @($storageComponents) + @($driveComponents) $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 } return [PSCustomObject]@{ HostName = $VMHost.Name ClusterName = $cluster Components = Group-VcfCheckVsanHclComponentsByModel -Components $components NotListedComponents = @($components | Where-Object { $_.Status -eq 'NotListed' -and $_.CurrentlyUsedByvSAN } | Select-Object -ExcludeProperty SupportedVsanTypes -Property *) FirmwareUnsupportedComponents = @($components | Where-Object { $_.Status -eq 'FirmwareUnsupported' -and $_.CurrentlyUsedByvSAN } | Select-Object -ExcludeProperty SupportedVsanTypes -Property *) IncompatibleWithTargetReleaseComponents = @($components | Where-Object { $_.Status -eq 'IncompatibleWithTargetRelease' -and $_.CurrentlyUsedByvSAN } | Select-Object -ExcludeProperty SupportedVsanTypes -Property *) } } |