Private/VsanHclHelpers.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. # # ============================================================================= #region VsanHclHelpers $Script:VcfCheckVsanHclDataCache = $null $Script:VcfCheckVsanHclDataIndexCache = $null $Script:VcfCheckVsanHclDriveDataCache = $null $Script:VcfCheckVsanHclDriveDataIndexCache = $null $Script:VcfCheckVsanHclLinksCache = $null function Get-VcfCheckVsanHclData { <# .SYNOPSIS Loads and caches the shipped vSAN Hardware Compatibility List snapshot (Data/VsanHcl/VsanHclCompatibility.json). .DESCRIPTION Reads a snapshot captured by InternalTools/Update-VcfVsanHclData.ps1 at release-prep time from the public feed backing https://vvs.broadcom.com/service/vsan/all.json, so Resolve-VcfCheckVsanHclCompatibility can match host storage controller/NIC PCI identities against the vSAN HCL entirely offline. .OUTPUTS [PSObject[]] array of controller/NIC entries (.id, .vid, .did, .ssid, .svid, .rel), or an empty array if the file is missing or fails to parse - callers treat that as "could not confirm," never as "not compatible." #> [CmdletBinding()] [OutputType([PSObject[]])] Param () if ($null -eq $Script:VcfCheckVsanHclDataCache) { $data = @() $path = Join-Path -Path $PSScriptRoot -ChildPath (Join-Path -Path '..' -ChildPath (Join-Path -Path 'Data' -ChildPath (Join-Path -Path 'VsanHcl' -ChildPath 'VsanHclCompatibility.json'))) if (Test-Path -LiteralPath $path -PathType Leaf) { try { $data = @(Get-Content -LiteralPath $path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop) } catch { Write-LogMessage -Type WARNING -Message "Could not parse vSAN HCL data file `"$path`": $($_.Exception.Message)" $data = @() } } else { Write-LogMessage -Type WARNING -Message "No shipped vSAN HCL data file found: `"$path`" not found" } $Script:VcfCheckVsanHclDataCache = $data } return $Script:VcfCheckVsanHclDataCache } function Get-VcfCheckVsanHclDataIndex { <# .SYNOPSIS Builds and caches a PCI-identity lookup index over the shipped vSAN HCL controller/NIC snapshot, so Resolve-VcfCheckVsanHclCompatibility can match in O(1) instead of scanning every shipped entry per host component. .DESCRIPTION Keys Get-VcfCheckVsanHclData's entries by "vid|did|svid|ssid"; entries with no vid are skipped since Resolve-VcfCheckVsanHclCompatibility never looks one up with an empty VendorId. When more than one entry shares a key, the first one encountered wins, matching the prior Where-Object | Select-Object -First 1 behavior. .OUTPUTS [Hashtable] keyed by "vid|did|svid|ssid" to the matching HCL entry. #> [CmdletBinding()] [OutputType([Hashtable])] Param () if ($null -eq $Script:VcfCheckVsanHclDataIndexCache) { $index = @{} foreach ($entry in @(Get-VcfCheckVsanHclData)) { if ([String]::IsNullOrEmpty($entry.vid)) { continue } $key = "$($entry.vid)|$($entry.did)|$($entry.svid)|$($entry.ssid)" if (-not $index.ContainsKey($key)) { $index[$key] = $entry } } $Script:VcfCheckVsanHclDataIndexCache = $index } return $Script:VcfCheckVsanHclDataIndexCache } function Get-VcfCheckVsanHclDriveData { <# .SYNOPSIS Loads and caches the shipped vSAN Hardware Compatibility List drive snapshot (Data/VsanHcl/VsanHclDriveCompatibility.json). .DESCRIPTION Reads a snapshot captured by InternalTools/Update-VcfVsanHclData.ps1 at release-prep time from the public feed backing https://vvs.broadcom.com/service/vsan/all.json, so Resolve-VcfCheckVsanHclDriveCompatibility can match host SSD/HDD drives against the vSAN HCL entirely offline. Every entry is keyed by trimmed, lowercased model string, with a trimmed, lowercased productid string as a secondary key for entries whose model is a marketing description rather than the terse string ESX actually reports; NVMe drive entries (roughly a third of the asset) also carry vid/did/svid/ssid, since an NVMe drive is its own PCIe endpoint rather than sitting behind a shared HBA like SAS/SATA. .OUTPUTS [PSObject[]] array of drive entries (.id, .vendor, .model, .productid, .vid, .did, .svid, .ssid, .rel - the PCI id and productid properties are empty strings when the feed had none for that entry), or an empty array if the file is missing or fails to parse - callers treat that as "could not confirm," never as "not compatible." #> [CmdletBinding()] [OutputType([PSObject[]])] Param () if ($null -eq $Script:VcfCheckVsanHclDriveDataCache) { $data = @() $path = Join-Path -Path $PSScriptRoot -ChildPath (Join-Path -Path '..' -ChildPath (Join-Path -Path 'Data' -ChildPath (Join-Path -Path 'VsanHcl' -ChildPath 'VsanHclDriveCompatibility.json'))) if (Test-Path -LiteralPath $path -PathType Leaf) { try { $data = @(Get-Content -LiteralPath $path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop) } catch { Write-LogMessage -Type WARNING -Message "Could not parse vSAN HCL drive data file `"$path`": $($_.Exception.Message)" $data = @() } } else { Write-LogMessage -Type WARNING -Message "No shipped vSAN HCL drive data file found: `"$path`" not found" } $Script:VcfCheckVsanHclDriveDataCache = $data } return $Script:VcfCheckVsanHclDriveDataCache } function Get-VcfCheckVsanHclDriveDataIndex { <# .SYNOPSIS Builds and caches PCI-identity, model, and productid lookup indexes over the shipped vSAN HCL drive snapshot, so Resolve-VcfCheckVsanHclDriveCompatibility can match in O(1) instead of scanning every shipped entry per host drive. .DESCRIPTION Keys Get-VcfCheckVsanHclDriveData's entries three ways: PciIndex by "vid|did|svid|ssid" (entries with no vid are skipped, mirroring Get-VcfCheckVsanHclDataIndex), ModelIndex by the entry's already-normalized model string (first entry wins on a collision, matching the prior Where-Object | Select-Object -First 1 behavior), and ProductIdIndex by productid to an array of every entry sharing it, since Resolve-VcfCheckVsanHclDriveCompatibility narrows productid matches by vendor rather than taking the first one. .OUTPUTS [PSCustomObject] with PciIndex, ModelIndex (both [Hashtable] entry-by-key), and ProductIdIndex ([Hashtable] entry-array-by-key). #> [CmdletBinding()] [OutputType([PSObject])] Param () if ($null -eq $Script:VcfCheckVsanHclDriveDataIndexCache) { $pciIndex = @{} $modelIndex = @{} $productIdIndex = @{} foreach ($entry in @(Get-VcfCheckVsanHclDriveData)) { if (-not [String]::IsNullOrEmpty($entry.vid)) { $pciKey = "$($entry.vid)|$($entry.did)|$($entry.svid)|$($entry.ssid)" if (-not $pciIndex.ContainsKey($pciKey)) { $pciIndex[$pciKey] = $entry } } if (-not [String]::IsNullOrEmpty($entry.model) -and -not $modelIndex.ContainsKey($entry.model)) { $modelIndex[$entry.model] = $entry } if (-not [String]::IsNullOrEmpty($entry.productid)) { if (-not $productIdIndex.ContainsKey($entry.productid)) { $productIdIndex[$entry.productid] = @() } $productIdIndex[$entry.productid] += $entry } } $Script:VcfCheckVsanHclDriveDataIndexCache = [PSCustomObject]@{ PciIndex = $pciIndex; ModelIndex = $modelIndex; ProductIdIndex = $productIdIndex } } return $Script:VcfCheckVsanHclDriveDataIndexCache } function Get-VcfCheckVsanHclLinkData { <# .SYNOPSIS Loads and caches the shipped vSAN HCL id-to-VCG-program side table (Data/VsanHcl/VsanHclLinks.json). .DESCRIPTION Reads a side table captured by InternalTools/Update-VcfVsanHclData.ps1 alongside the main match assets. Every VCG "view in Compatibility Guide" link is https://compatibilityguide.broadcom.com/detail?program=<program>&productId=<id>&persona=live with productId always equal to the entry's own id, so this table only needs to carry the per-id program value (e.g. "ssd", "hdd", "rdmanic", "vsanio") rather than the full URL; Get-VcfCheckVsanHclLink reconstructs the URL from it. .OUTPUTS [PSObject] property bag keyed by HCL entry id (string) with the matching program value, or an empty PSObject if the file is missing or fails to parse. #> [CmdletBinding()] [OutputType([PSObject])] Param () if ($null -eq $Script:VcfCheckVsanHclLinksCache) { $data = [PSCustomObject]@{} $path = Join-Path -Path $PSScriptRoot -ChildPath (Join-Path -Path '..' -ChildPath (Join-Path -Path 'Data' -ChildPath (Join-Path -Path 'VsanHcl' -ChildPath 'VsanHclLinks.json'))) if (Test-Path -LiteralPath $path -PathType Leaf) { try { $data = Get-Content -LiteralPath $path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { Write-LogMessage -Type WARNING -Message "Could not parse vSAN HCL links file `"$path`": $($_.Exception.Message)" $data = [PSCustomObject]@{} } } else { Write-LogMessage -Type WARNING -Message "No shipped vSAN HCL links file found: `"$path`" not found" } $Script:VcfCheckVsanHclLinksCache = $data } return $Script:VcfCheckVsanHclLinksCache } function Get-VcfCheckVsanHclLink { <# .SYNOPSIS Reconstructs the "view in VCG" URL for a matched vSAN HCL entry id. .DESCRIPTION Looks up Id's program value via Get-VcfCheckVsanHclLinkData and formats it back into the Broadcom Compatibility Guide detail URL. Id is used as-is for productId, matching how InternalTools/Update-VcfVsanHclData.ps1 populated the side table. .PARAMETER Id The matched HCL entry's numeric id (as returned in .MatchedId by Resolve-VcfCheckVsanHclCompatibility or Resolve-VcfCheckVsanHclDriveCompatibility). .OUTPUTS [String] the full VCG detail URL, or $null if Id has no entry in the links side table. #> [CmdletBinding()] [OutputType([String])] Param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Id ) $links = Get-VcfCheckVsanHclLinkData $program = $links.$Id if ([String]::IsNullOrEmpty($program)) { return $null } return "https://compatibilityguide.broadcom.com/detail?program=$program&productId=$Id&persona=live" } function Resolve-VcfCheckVsanHclOlderFamily { <# .SYNOPSIS Finds the newest pre-target-family ESX release a matched vSAN HCL entry is certified for. .DESCRIPTION Used by Resolve-VcfCheckVsanHclCompatibility and Resolve-VcfCheckVsanHclDriveCompatibility when a matched entry has no release data for the target ESX family (e.g. '9.1'), to tell "on the HCL but only certified for an older release" (IncompatibleWithTargetRelease) apart from "no release data at all for this entry" (Unknown). Shipped release keys for a given major version can include update releases (e.g. '8.0', '8.0 U1', '8.0 U2', '8.0 U3'); the highest key by ordinary string sort is returned with its update suffix stripped, since a component certified for '8.0 U3' is also meaningfully described as an '8.0.x' device to an operator. .PARAMETER Rel The matched HCL entry's .rel property (PSCustomObject keyed by normalized release, e.g. '9.1', '8.0 U3'), or $null. .PARAMETER TargetFamily The target ESX major.minor family being checked against, e.g. '9.1' - release keys starting with this family's major version digit are excluded from consideration. .OUTPUTS [String] the newest older major.minor family found (e.g. '8.0'), or $null if Rel is $null, has no properties, or has no release key outside TargetFamily's major version. #> [CmdletBinding()] [OutputType([String])] Param ( [Parameter(Mandatory = $false)] [AllowNull()] [PSObject]$Rel, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$TargetFamily ) if ($null -eq $Rel) { return $null } $targetMajor = ($TargetFamily -split '\.')[0] $olderKeys = @($Rel.PSObject.Properties.Name | Where-Object { ($_ -split '\.')[0] -ne $targetMajor }) if ($olderKeys.Count -eq 0) { return $null } $newestOlderKey = $olderKeys | Sort-Object -Descending | Select-Object -First 1 return ($newestOlderKey -split ' ')[0] } function Resolve-VcfCheckVsanHclCompatibility { <# .SYNOPSIS Flags whether an ESX host's storage controller or NIC appears on Broadcom's published vSAN Hardware Compatibility List for a given ESX release, and whether its driver/firmware is a supported combination. .DESCRIPTION Matches VendorId/DeviceId/SubVendorId/SubDeviceId against each shipped HCL entry's vid/did/svid/ssid (see InternalTools/Update-VcfVsanHclData.ps1 for how the asset is trimmed from the live feed). ESXi 9.0/9.1 data drives this check's Compatible/ FirmwareUnsupported verdicts; ESXi 8.0.x data is also shipped solely so a component with no 9.x release data can be told apart as IncompatibleWithTargetRelease rather than Unknown (see Resolve-VcfCheckVsanHclOlderFamily). When DriverName/DriverVersion/FirmwareVersion are supplied, the match drills into the entry's per-driver firmware list; when they are omitted, the release-level supported vSAN types are returned without a driver/firmware-specific verdict. .PARAMETER VendorId The host PCI VendorId, normalized lowercase 4-hex-digit (e.g. "1000"). .PARAMETER DeviceId The host PCI DeviceId, normalized lowercase 4-hex-digit. .PARAMETER SubVendorId The host PCI SubVendorId, normalized lowercase 4-hex-digit. .PARAMETER SubDeviceId The host PCI SubDeviceId, normalized lowercase 4-hex-digit. .PARAMETER EsxVersion The ESX release family to check against, e.g. '9.0' or '9.1'. .PARAMETER DriverName The driver module name in use on the host, e.g. "lsi_msgpt3". Optional - omit to skip the driver/firmware-specific match and only resolve release-level HCL presence. .PARAMETER DriverVersion The driver version in use on the host. Optional, ignored unless DriverName is supplied. .PARAMETER FirmwareVersion The firmware version in use on the host. Optional, ignored unless DriverName and DriverVersion are supplied. .OUTPUTS [PSObject] with .Status ('Compatible', 'FirmwareUnsupported', 'IncompatibleWithTargetRelease', 'NotListed', or 'Unknown'), .SupportedVsanTypes (string array, e.g. "All Flash:Pass-Through"), .MatchedId (the matched HCL entry's numeric id, or $null), and .LatestCompatibleRelease (the ESX major.minor family the component is certified for, e.g. '9.0', when Status is 'Compatible' or 'FirmwareUnsupported'; the newest older family it IS certified for, e.g. '8.0', when Status is 'IncompatibleWithTargetRelease'; $null otherwise). 'NotListed' means no shipped entry's PCI identity matched - this may be a genuinely unsupported component, or this check's shipped data is out of date; it is not a certified verdict. 'FirmwareUnsupported' means the component matched but the supplied driver/version/firmware combination did not. 'IncompatibleWithTargetRelease' means the component matched an HCL entry that has release data for an older ESX family but none for EsxVersion's family - it is certified, just not for the target release. 'Unknown' means VendorId/DeviceId was empty, EsxVersion could not be parsed to a major.minor family, no HCL data could be loaded, or the matched entry has no release data for EsxVersion's family or any older family shipped. #> [CmdletBinding()] [OutputType([PSObject])] Param ( [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$VendorId, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$DeviceId, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$SubVendorId, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$SubDeviceId, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$EsxVersion, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$DriverName, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$DriverVersion, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$FirmwareVersion ) $unknown = [PSCustomObject]@{ Status = 'Unknown'; SupportedVsanTypes = @(); MatchedId = $null; LatestCompatibleRelease = $null } if ([String]::IsNullOrWhiteSpace($VendorId) -or [String]::IsNullOrWhiteSpace($DeviceId)) { return $unknown } $esxFamily = [Regex]::Match($EsxVersion, '^\d+\.\d+').Value if ([String]::IsNullOrEmpty($esxFamily)) { return $unknown } $data = @(Get-VcfCheckVsanHclData) if ($data.Count -eq 0) { return $unknown } $matchedEntry = (Get-VcfCheckVsanHclDataIndex)["$VendorId|$DeviceId|$SubVendorId|$SubDeviceId"] if ($null -eq $matchedEntry) { return [PSCustomObject]@{ Status = 'NotListed'; SupportedVsanTypes = @(); MatchedId = $null; LatestCompatibleRelease = $null } } $release = $matchedEntry.rel.$esxFamily if ($null -eq $release) { $olderFamily = Resolve-VcfCheckVsanHclOlderFamily -Rel $matchedEntry.rel -TargetFamily $esxFamily if ($null -ne $olderFamily) { return [PSCustomObject]@{ Status = 'IncompatibleWithTargetRelease'; SupportedVsanTypes = @(); MatchedId = $matchedEntry.id; LatestCompatibleRelease = $olderFamily } } return $unknown } if ([String]::IsNullOrWhiteSpace($DriverName)) { return [PSCustomObject]@{ Status = 'Compatible'; SupportedVsanTypes = @($release.vs); MatchedId = $matchedEntry.id; LatestCompatibleRelease = $esxFamily } } $driverVersions = $release.drv.$DriverName $versionEntry = if ($driverVersions) { $driverVersions.$DriverVersion } else { $null } if ($null -eq $versionEntry) { return [PSCustomObject]@{ Status = 'FirmwareUnsupported'; SupportedVsanTypes = @(); MatchedId = $matchedEntry.id; LatestCompatibleRelease = $esxFamily } } $matchedFirmware = @($versionEntry.fws) | Where-Object { $_.fw -eq $FirmwareVersion } | Select-Object -First 1 if ($null -eq $matchedFirmware) { return [PSCustomObject]@{ Status = 'FirmwareUnsupported'; SupportedVsanTypes = @(); MatchedId = $matchedEntry.id; LatestCompatibleRelease = $esxFamily } } return [PSCustomObject]@{ Status = 'Compatible'; SupportedVsanTypes = @($matchedFirmware.vs); MatchedId = $matchedEntry.id; LatestCompatibleRelease = $esxFamily } } function Get-VcfCheckVsanHclDriveHbaPciIdentity { <# .SYNOPSIS Resolves an NVMe drive's PCI identity via its owning HBA, for vSAN HCL drive matching. .DESCRIPTION An NVMe drive is its own PCIe endpoint, so ESX enumerates it as its own single-LUN HBA (e.g. `vmhba6`) - unlike a SAS/SATA drive, which sits behind a shared HBA/RAID controller whose PCI identity describes the controller chip, not any one drive behind it. Joins Drive.HbaName (from Get-VcfCheckVMHostScsiDevices) to the matching entry in StorageAdapters (from Get-VcfCheckVMHostStorageAdapters) only when Drive.Type indicates NVMe, so a shared SAS/RAID HBA's identity is never misattributed to the drives behind it. .PARAMETER Drive One drive entry as returned by Get-VcfCheckVMHostScsiDevices. .PARAMETER StorageAdapters The host's storage adapters, as returned by Get-VcfCheckVMHostStorageAdapters. .OUTPUTS [PSCustomObject] with VendorId, DeviceId, SubVendorId, SubDeviceId - all '' when Drive is not NVMe or has no matching storage adapter. #> [CmdletBinding()] [OutputType([PSObject])] Param ( [Parameter(Mandatory = $true)] [PSObject]$Drive, [Parameter(Mandatory = $false)] [PSObject[]]$StorageAdapters = @() ) $empty = [PSCustomObject]@{ VendorId = ''; DeviceId = ''; SubVendorId = ''; SubDeviceId = '' } if ("$($Drive.Type)" -notlike 'NVMe *' -or [String]::IsNullOrEmpty($Drive.HbaName)) { return $empty } $owningHba = $StorageAdapters | Where-Object { $_.Name -eq $Drive.HbaName } | Select-Object -First 1 if (-not $owningHba) { return $empty } return [PSCustomObject]@{ VendorId = $owningHba.VendorId DeviceId = $owningHba.DeviceId SubVendorId = $owningHba.SubVendorId SubDeviceId = $owningHba.SubDeviceId } } function Resolve-VcfCheckVsanHclDriveCompatibility { <# .SYNOPSIS Flags whether an ESX host's SSD/HDD drive appears on Broadcom's published vSAN Hardware Compatibility List for a given ESX release, and whether its firmware revision is a supported combination. .DESCRIPTION Prefers matching the drive's PCI VendorId/DeviceId/SubVendorId/SubDeviceId (available only for NVMe drives, which are their own PCIe endpoint - see Get-VcfCheckVsanHclDriveHbaPciIdentity) against a shipped drive HCL entry's vid/did/svid/ ssid when all four are supplied and present on an entry, since OEMs frequently rebrand a drive's reported Model string without changing its underlying PCI identity. Falls back to matching the reported Model string (trimmed, case-insensitive) against each shipped drive HCL entry's model, then - since some HCL entries carry a marketing description in `model` rather than the terse string ESX actually reports (e.g. a Dell-rebranded Toshiba SAS SSD whose HCL `model` is "1920GB Solid State Drive..." while ESX reports "PX05SRB192Y", which matches the HCL entry's `productid` instead) - to matching Model against each shipped entry's productid. productid is not unique across entries (the same physical part is sometimes re-listed under multiple HCL ids, occasionally under different vendor strings), so productid matches are narrowed to entries whose vendor matches the supplied Vendor (case-insensitive) first when more than one productid match exists, otherwise the first match wins - this is the only option for SAS/SATA drives, which carry no PCI identity of their own (see InternalTools/Update-VcfVsanHclData.ps1 for how the asset is trimmed from the live feed). ESXi 9.0/9.1 data drives this check's Compatible/FirmwareUnsupported verdicts; ESXi 8.0.x data is also shipped solely so a drive with no 9.x release data can be told apart as IncompatibleWithTargetRelease rather than Unknown (see Resolve-VcfCheckVsanHclOlderFamily). Some HCL entries carry no firmware value at all (the feed's `firmware` field is blank), in which case any reported FirmwareRevision is accepted and the release-level supported vSAN types are returned; entries that do carry firmware values require FirmwareRevision to match one of them. .PARAMETER Model The drive's reported Model string, as returned by Get-VcfCheckVMHostScsiDevices. .PARAMETER Vendor The drive's reported Vendor string, as returned by Get-VcfCheckVMHostScsiDevices. Optional - only used to narrow a productid match when more than one shipped entry shares that productid; omitting it just means the first productid match wins ties. .PARAMETER VendorId The drive's owning HBA PCI VendorId, normalized lowercase 4-hex-digit. Optional - omit (or supply alongside an empty DeviceId/SubVendorId/SubDeviceId) to skip PCI-identity matching and match on Model alone. Only meaningful for NVMe drives (see Get-VcfCheckVsanHclDriveHbaPciIdentity) - passing a SAS/SATA HBA's shared PCI identity here would incorrectly attribute every drive behind that HBA with the controller's identity. .PARAMETER DeviceId The drive's owning HBA PCI DeviceId, normalized lowercase 4-hex-digit. .PARAMETER SubVendorId The drive's owning HBA PCI SubVendorId, normalized lowercase 4-hex-digit. .PARAMETER SubDeviceId The drive's owning HBA PCI SubDeviceId, normalized lowercase 4-hex-digit. .PARAMETER EsxVersion The ESX release family to check against, e.g. '9.0' or '9.1'. .PARAMETER FirmwareRevision The drive's reported firmware revision. Optional - omit to skip the firmware-specific match and only resolve release-level HCL presence. .OUTPUTS [PSObject] with .Status ('Compatible', 'FirmwareUnsupported', 'IncompatibleWithTargetRelease', 'NotListed', or 'Unknown'), .SupportedVsanTypes (string array, e.g. "AF-Cache"), .MatchedId (the matched HCL entry's numeric id, or $null), and .LatestCompatibleRelease (the ESX major.minor family the drive is certified for, e.g. '9.0', when Status is 'Compatible' or 'FirmwareUnsupported'; the newest older family it IS certified for, e.g. '8.0', when Status is 'IncompatibleWithTargetRelease'; $null otherwise). 'NotListed' means no shipped entry's PCI identity, model, or productid matched - this may be a genuinely unsupported drive, or this check's shipped data is out of date; it is not a certified verdict. 'FirmwareUnsupported' means the drive matched but the supplied firmware revision did not, for an entry that does track firmware. 'IncompatibleWithTargetRelease' means the drive matched an HCL entry that has release data for an older ESX family but none for EsxVersion's family - it is certified, just not for the target release. 'Unknown' means Model was empty with no usable PCI identity supplied, EsxVersion could not be parsed to a major.minor family, no HCL drive data could be loaded, or the matched entry has no release data for EsxVersion's family or any older family shipped. #> [CmdletBinding()] [OutputType([PSObject])] Param ( [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$Model, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$Vendor, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$VendorId, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$DeviceId, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$SubVendorId, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$SubDeviceId, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$EsxVersion, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [String]$FirmwareRevision ) $unknown = [PSCustomObject]@{ Status = 'Unknown'; SupportedVsanTypes = @(); MatchedId = $null; LatestCompatibleRelease = $null } $hasPciIdentity = -not ([String]::IsNullOrWhiteSpace($VendorId) -or [String]::IsNullOrWhiteSpace($DeviceId) -or ` [String]::IsNullOrWhiteSpace($SubVendorId) -or [String]::IsNullOrWhiteSpace($SubDeviceId)) if (-not $hasPciIdentity -and [String]::IsNullOrWhiteSpace($Model)) { return $unknown } $esxFamily = [Regex]::Match($EsxVersion, '^\d+\.\d+').Value if ([String]::IsNullOrEmpty($esxFamily)) { return $unknown } $data = @(Get-VcfCheckVsanHclDriveData) if ($data.Count -eq 0) { return $unknown } $index = Get-VcfCheckVsanHclDriveDataIndex $matchedEntry = $null if ($hasPciIdentity) { $matchedEntry = $index.PciIndex["$VendorId|$DeviceId|$SubVendorId|$SubDeviceId"] } if ($null -eq $matchedEntry -and -not [String]::IsNullOrWhiteSpace($Model)) { $normalizedModel = $Model.Trim().ToLowerInvariant() $matchedEntry = $index.ModelIndex[$normalizedModel] if ($null -eq $matchedEntry) { $productIdMatches = @(if ($index.ProductIdIndex.ContainsKey($normalizedModel)) { $index.ProductIdIndex[$normalizedModel] }) if ($productIdMatches.Count -gt 1 -and -not [String]::IsNullOrWhiteSpace($Vendor)) { $normalizedVendor = $Vendor.Trim().ToLowerInvariant() $vendorNarrowed = @($productIdMatches | Where-Object { "$($_.vendor)".Trim().ToLowerInvariant() -eq $normalizedVendor }) if ($vendorNarrowed.Count -gt 0) { $productIdMatches = $vendorNarrowed } } $matchedEntry = $productIdMatches | Select-Object -First 1 } } if ($null -eq $matchedEntry) { return [PSCustomObject]@{ Status = 'NotListed'; SupportedVsanTypes = @(); MatchedId = $null; LatestCompatibleRelease = $null } } $release = $matchedEntry.rel.$esxFamily if ($null -eq $release) { $olderFamily = Resolve-VcfCheckVsanHclOlderFamily -Rel $matchedEntry.rel -TargetFamily $esxFamily if ($null -ne $olderFamily) { return [PSCustomObject]@{ Status = 'IncompatibleWithTargetRelease'; SupportedVsanTypes = @(); MatchedId = $matchedEntry.id; LatestCompatibleRelease = $olderFamily } } return $unknown } $trackedFirmwares = @($release.fws | Where-Object { -not [String]::IsNullOrEmpty($_.fw) }) if ($trackedFirmwares.Count -eq 0) { return [PSCustomObject]@{ Status = 'Compatible'; SupportedVsanTypes = @($release.vs); MatchedId = $matchedEntry.id; LatestCompatibleRelease = $esxFamily } } $matchedFirmware = $trackedFirmwares | Where-Object { $_.fw -eq $FirmwareRevision } | Select-Object -First 1 if ($null -eq $matchedFirmware) { return [PSCustomObject]@{ Status = 'FirmwareUnsupported'; SupportedVsanTypes = @(); MatchedId = $matchedEntry.id; LatestCompatibleRelease = $esxFamily } } return [PSCustomObject]@{ Status = 'Compatible'; SupportedVsanTypes = @($matchedFirmware.vs); MatchedId = $matchedEntry.id; LatestCompatibleRelease = $esxFamily } } function Get-VcfCheckVsanHclHostMemberDiskCanonicalNames { <# .SYNOPSIS Returns the canonical names of a host's drives that belong to a vSAN disk group. .DESCRIPTION Reads the host's vCenter's vSAN disk group inventory (Get-VcfCheckVsanDiskGroupInventory), cached per vCenter FQDN on $Context.VsanDiskGroupInventoryCache since every host in a cluster shares the same disk group list, filters to the disk groups owned by VMHost, and collects the CanonicalName of each member disk (Get-VcfCheckVsanDiskInventory) - the same identity Get-VcfCheckVMHostScsiDevices reports as .Name, so Get-VcfCheckVsanHclHostDetail can flag whether each drive is currently used by vSAN. vSAN ESA has no disk groups at all - a storage pool's drives are never reported by Get-VsanDiskGroup - so when VMHost has no disk groups, this falls back to esxcli's vsan.storage.list (Get-VcfCheckVsanStorageListForHost), keeping every drive esxcli reports as "Used by this host", cached per vCenter FQDN + host name on $Context.VsanEsaMemberDiskNameCache since it is a per-host esxcli round-trip. A vCenter with no vSAN configured, or a lookup failure in either source, yields an empty set - callers treat that as "not a vSAN member," not an error, since this is a supplementary flag on the HCL check, not its subject. .PARAMETER Context The VcfCheck.Context object, used to cache the per-vCenter disk group inventory and the per-host esxcli fallback. .PARAMETER VMHost A VMHost inventory object retrieved via Get-VcfCheckVMHostInventory. .OUTPUTS [String[]] canonical names (e.g. "naa.xxx", "eui.xxx") of VMHost's drives that belong to a vSAN disk group, or - for a vSAN ESA host with no disk groups - that esxcli reports as used by the host, or an empty array. #> [CmdletBinding()] [OutputType([String[]])] Param ( [Parameter(Mandatory = $true)] [PSObject]$Context, [Parameter(Mandatory = $true)] [PSObject]$VMHost ) $vcenterFqdn = Get-VcfCheckVMHostVCenterFqdn -VMHost $VMHost if (-not $Context.VsanDiskGroupInventoryCache.ContainsKey($vcenterFqdn)) { $diskGroups = try { @(Get-VcfCheckVsanDiskGroupInventory -Server $vcenterFqdn) } catch { Write-LogMessage -Type WARNING -Message "Could not retrieve vSAN disk group inventory for `"$vcenterFqdn`": $($_.Exception.Message)" @() } $Context.VsanDiskGroupInventoryCache[$vcenterFqdn] = $diskGroups } $hostDiskGroups = @($Context.VsanDiskGroupInventoryCache[$vcenterFqdn] | Where-Object { $_.VMHost.Name -eq $VMHost.Name }) if ($hostDiskGroups.Count -gt 0) { return @($hostDiskGroups | ForEach-Object { try { Get-VcfCheckVsanDiskInventory -VsanDiskGroup $_ } catch { @() } } | ForEach-Object { $_.CanonicalName } | Where-Object { -not [String]::IsNullOrEmpty($_) }) } $cacheKey = "$vcenterFqdn|$($VMHost.Name)" if (-not $Context.VsanEsaMemberDiskNameCache.ContainsKey($cacheKey)) { $esaMemberDiskNames = try { @(Get-VcfCheckVsanStorageListForHost -VMHost $VMHost | Where-Object { "$(Get-VcfCheckEsxCliPropertyValue -InputObject $_ -Name 'Used by this host')" -eq 'true' } | ForEach-Object { Get-VcfCheckEsxCliPropertyValue -InputObject $_ -Name 'Device' } | Where-Object { -not [String]::IsNullOrEmpty($_) }) } catch { Write-LogMessage -Type WARNING -Message "Could not retrieve vSAN ESA storage pool membership for `"$($VMHost.Name)`": $($_.Exception.Message)" @() } $Context.VsanEsaMemberDiskNameCache[$cacheKey] = $esaMemberDiskNames } return $Context.VsanEsaMemberDiskNameCache[$cacheKey] } function Get-VcfCheckVsanHclHostInUseDeviceNames { <# .SYNOPSIS Returns the storage controller and network adapter device names currently used by vSAN on a host. .DESCRIPTION Storage controllers: reads vsan.debug.controller.list filtered to controllers used by vSAN (Get-VcfCheckVsanControllerListForHost, the esxcli equivalent of `esxcli vsan debug controller list --used-by-vsan`) and returns each controller's device name (e.g. "vmhba0") - the same identity Get-VcfCheckVMHostStorageAdapters reports as .Name. Network adapters: reads vsan.network.list (Get-VcfCheckVsanNetworkListForHost) to find the VMkernel adapter(s) carrying vSAN traffic, resolves each to its port group (Get-VMHostNetworkAdapter), and returns the physical uplink(s) backing that port group - the active/standby NICs of the port group's teaming policy for a standard vSwitch (Get-VirtualPortGroup | Get-NicTeamingPolicy), or every physical uplink of the owning distributed switch as a best-effort fallback when the port group is distributed, since no PowerCLI cmdlet reports a distributed port group's active uplinks directly. Both esxcli round trips and the network-to-uplink resolution are cached per vCenter FQDN + host name on $Context.VsanInUseDeviceNameCache, since Get-VcfCheckVsanHclHostDetail is the only caller and should only pay for them once per host per run. A lookup failure for either device type yields an empty set for that type - callers treat that as "not currently in use," not an error, since this is a supplementary flag on the HCL check, not its subject. .PARAMETER Context The VcfCheck.Context object, used to cache the per-host in-use device names. .PARAMETER VMHost A VMHost inventory object retrieved via Get-VcfCheckVMHostInventory. .OUTPUTS [PSCustomObject] with ControllerNames and NetworkAdapterNames ([String[]] each). #> [CmdletBinding()] [OutputType([PSObject])] Param ( [Parameter(Mandatory = $true)] [PSObject]$Context, [Parameter(Mandatory = $true)] [PSObject]$VMHost ) $vcenterFqdn = Get-VcfCheckVMHostVCenterFqdn -VMHost $VMHost $cacheKey = "$vcenterFqdn|$($VMHost.Name)" if ($Context.VsanInUseDeviceNameCache.ContainsKey($cacheKey)) { return $Context.VsanInUseDeviceNameCache[$cacheKey] } $controllerNames = try { @(Get-VcfCheckVsanControllerListForHost -VMHost $VMHost | ForEach-Object { Get-VcfCheckVsanHclEsxCliFieldValue -EsxCliResult $_ -CandidateNames @('Device', 'VmhbaName', 'DeviceName', 'Name') } | Where-Object { -not [String]::IsNullOrEmpty($_) }) } catch { Write-LogMessage -Type WARNING -Message "Could not retrieve vSAN controller usage for `"$($VMHost.Name)`": $($_.Exception.Message)" @() } $networkAdapterNames = try { $vmkNicNames = @(Get-VcfCheckVsanNetworkListForHost -VMHost $VMHost | ForEach-Object { Get-VcfCheckVsanHclEsxCliFieldValue -EsxCliResult $_ -CandidateNames @('VmkNicName', 'InterfaceName', 'VmknicName') } | Where-Object { -not [String]::IsNullOrEmpty($_) }) @($vmkNicNames | ForEach-Object { Resolve-VcfCheckVsanHclVmkNicUplinkNames -VMHost $VMHost -VmkNicName $_ } | Select-Object -Unique) } catch { Write-LogMessage -Type WARNING -Message "Could not retrieve vSAN network usage for `"$($VMHost.Name)`": $($_.Exception.Message)" @() } $inUseDeviceNames = [PSCustomObject]@{ ControllerNames = $controllerNames NetworkAdapterNames = $networkAdapterNames } $Context.VsanInUseDeviceNameCache[$cacheKey] = $inUseDeviceNames return $inUseDeviceNames } function Get-VcfCheckVsanHclEsxCliFieldValue { <# .SYNOPSIS Reads the first present property from an esxcli V2 Invoke() result, tolerating the display-name-derived property naming that varies across ESX releases. .PARAMETER EsxCliResult A single row object returned by an esxcli V2 Invoke() call. .PARAMETER CandidateNames Property names to try, in order of preference. .OUTPUTS [String] the first matching property's value, or $null if none of CandidateNames is present. #> [CmdletBinding()] [OutputType([String])] Param ( [Parameter(Mandatory = $true)] [PSObject]$EsxCliResult, [Parameter(Mandatory = $true)] [String[]]$CandidateNames ) $propertyNames = @($EsxCliResult.PSObject.Properties.Name) foreach ($candidateName in $CandidateNames) { if ($propertyNames -contains $candidateName) { return "$($EsxCliResult.$candidateName)" } } return $null } function Resolve-VcfCheckVsanHclVmkNicUplinkNames { <# .SYNOPSIS Resolves the physical NIC(s) backing a VMkernel adapter's port group. .DESCRIPTION Looks up the VMkernel adapter's port group (Get-VcfCheckVMHostVmkNicPortGroupName) and, for a standard vSwitch port group, returns its teaming policy's active and standby physical NICs (Get-VcfCheckStandardPortGroupTeamingNicNames). For a distributed port group, falls back to every physical uplink of the owning distributed switch (Get-VcfCheckDistributedPortGroupUplinkNames) - a best-effort superset, not the exact active-teaming subset, since no PowerCLI cmdlet reports a distributed port group's active uplinks directly. A lookup failure at any stage yields an empty set, not an error, since this is a supplementary flag on the HCL check, not its subject. .PARAMETER VMHost A VMHost inventory object retrieved via Get-VcfCheckVMHostInventory. .PARAMETER VmkNicName The VMkernel adapter's device name (e.g. "vmk1"), as reported by vsan.network.list. .OUTPUTS [String[]] physical NIC device names (e.g. "vmnic0"), or an empty array if the adapter or its port group could not be resolved. #> [CmdletBinding()] [OutputType([String[]])] Param ( [Parameter(Mandatory = $true)] [PSObject]$VMHost, [Parameter(Mandatory = $true)] [String]$VmkNicName ) $portGroupName = try { Get-VcfCheckVMHostVmkNicPortGroupName -VMHost $VMHost -Name $VmkNicName } catch { $null } if ([String]::IsNullOrEmpty($portGroupName)) { return @() } $standardUplinkNames = try { @(Get-VcfCheckStandardPortGroupTeamingNicNames -VMHost $VMHost -PortGroupName $portGroupName) } catch { @() } if ($standardUplinkNames.Count -gt 0) { return @($standardUplinkNames | Select-Object -Unique) } $distributedUplinkNames = try { @(Get-VcfCheckDistributedPortGroupUplinkNames -VMHost $VMHost -PortGroupName $portGroupName) } catch { @() } return @($distributedUplinkNames | Select-Object -Unique) } function Group-VcfCheckVsanHclComponentsByModel { <# .SYNOPSIS Collapses a host's per-device vSAN HCL component verdicts into one row per distinct Vendor/Model/Status combination, for display. .DESCRIPTION A host with several identical NICs or drives otherwise produces one component row per device (Name vmnic0, vmnic1, ...), which makes the vSAN HCL Compliance Check's per-host component table far longer than it needs to be. Groups by DeviceType/Vendor/Model/Status/ SupportedVsanTypes/LatestCompatibleRelease/CurrentlyUsedByvSAN (not just Vendor/Model, since identical models can carry different driver/firmware and therefore different verdicts, or one device can currently be used by vSAN while an identical device in the same host is not) and rolls the grouped device names into a single comma-delimited Devices property. .PARAMETER Components Array of per-component PSCustomObjects as built by Get-VcfCheckVsanHclHostDetail (.Name, .DeviceType, .Vendor, .Model, .Status, .SupportedVsanTypes, .LatestCompatibleRelease, .CurrentlyUsedByvSAN). .OUTPUTS [PSCustomObject[]] one row per distinct DeviceType/Vendor/Model/Status/SupportedVsanTypes/ LatestCompatibleRelease/CurrentlyUsedByvSAN combination, with .Devices (comma-delimited, sorted device names) in place of .Name. #> [CmdletBinding()] [OutputType([PSObject[]])] Param ( [Parameter(Mandatory = $false)] [AllowNull()] [Object[]]$Components = @() ) $groups = [Ordered]@{} foreach ($component in @($Components)) { $vsanTypesKey = ($component.SupportedVsanTypes -join ',') $key = @($component.DeviceType, $component.Vendor, $component.Model, $component.Status, $vsanTypesKey, $component.LatestCompatibleRelease, $component.CurrentlyUsedByvSAN) -join '|' if (-not $groups.Contains($key)) { $groups[$key] = [Ordered]@{ DeviceType = $component.DeviceType Vendor = $component.Vendor Model = $component.Model Status = $component.Status SupportedVsanTypes = $component.SupportedVsanTypes LatestCompatibleRelease = $component.LatestCompatibleRelease CurrentlyUsedByvSAN = $component.CurrentlyUsedByvSAN DeviceNames = [System.Collections.Generic.List[String]]::new() } } $groups[$key].DeviceNames.Add($component.Name) } return ,@($groups.Values | ForEach-Object { [PSCustomObject]@{ DeviceType = $_.DeviceType Vendor = $_.Vendor Model = $_.Model Devices = (($_.DeviceNames | Sort-Object) -join ', ') Status = $_.Status SupportedVsanTypes = $_.SupportedVsanTypes LatestCompatibleRelease = $_.LatestCompatibleRelease CurrentlyUsedByvSAN = $_.CurrentlyUsedByvSAN } }) } #endregion |