Private/Checks/SddcManager/Test-VcfSddcBomCheck.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. # # ============================================================================= $Script:VcfCheckDefaultDestinationRelease = 'latest' function Test-VcfSddcBomCheck { <# .SYNOPSIS Validates SDDC Manager, vCenters, ESX Hosts/Clusters, and NSX components across all VCF domains against required minimum release thresholds. .DESCRIPTION Queries SDDC Manager and all workload domains to build a detailed compliance report. Aggregates host compliance at the cluster level when versions are identical, or expands cluster details per host when version drift is detected. Every component is judged independently against the destination release's own requirement - a disjoint sBOM (e.g. vCenter already ahead of the target while NSX sits exactly at its floor) is expected and fine; this never requires every component to match one release's exact version set. Each component's Compliant verdict is decided in this priority order (see New-VcfCheckBomComplianceRow): 1. PRIMARY - Broadcom's public Interop Matrix's own real per-version-pair upgrade-path status (Get-VcfCheckInteropMatrixCompatibilityVerdict, with FailureReason detail from Get-VcfCheckInteropMatrixFailureReason), not a floor comparison. A floor only approximates compatibility; the matrix encodes real exceptions a floor cannot see - confirmed live: ESX build 25595708 ("8.0U3k") is Incompatible with VCF ESX release 9.1.0.0100 because 8.0U3k's own release date is later than 9.1.0.0100's (a release-date-based exception), even though a floor comparison would call it compliant. For ESX/vCenter, resolving the installed raw build to its Interop Matrix row requires the marketing-name alias data shipped in Data/MarketingVersionAliases/EsxMarketingVersionAliases.json / Data/MarketingVersionAliases/VcenterMarketingVersionAliases.json (regenerate via InternalTools/Update-VcfMarketingVersionAliases.ps1); NSX/SDDC Manager match by parsed version directly, no alias needed. 2. FALLBACK - a floor (>=) comparison against MinimumVersion, used only when the shipped Interop Matrix snapshot (Data/Interoperability/*.json) has no resolvable verdict for this exact pair: the installed build couldn't be resolved to a matrix row, or the specific pair isn't listed. Each Rows entry's FailureReason text distinguishes which path produced a non-compliant ReadyforUpgrade value, so a report reader can tell a proven Interop Matrix verdict from a minimum-version approximation. MinimumVersion itself (the floor's threshold) is resolved live, in this order: 1. Get-VcfCheckReleaseBom -Version <release> - the connected SDDC Manager's own release catalog. Exact, and in SDDC Manager's own version-string format, so no build-number-format mismatch risk. 2. If SDDC Manager's local catalog has no entry for that release (e.g. an offline/ air-gapped instance that hasn't synced it yet), falls back to this check's default floor thresholds (SDDC Manager 5.2, vCenter/ESX 8.0.3, NSX 4.2) and adds a Note recording whether Broadcom's public Interop Matrix at least confirms -VcfDestinationRelease is a real, published SDDC Manager release (a sanity check only, not a compatibility judgment - see Test-VcfCheckInteropMatrixVersionPublished). That confirmatory call requires internet access; if it also fails (or none is available), the Note says so and the default floor thresholds are still used - this check never fails or errors out solely because that lookup didn't succeed. A row that the Interop Matrix itself confirms Incompatible blocks the check (Status 'Fail', Blocking $true) - the destination release genuinely does not support that component. A row that only misses the MinimumVersion floor, with no Interop Matrix verdict either way, is reported as Status 'Warning' instead and does not block - the shipped Interop Matrix snapshot may simply not have caught up to a newly-published release yet, so a floor miss alone is not treated as confirmed non-compliance. Returns a VcfCheck.Result object containing a structured 'Rows' table detailing Domain, Component, Target, InstalledVersion, MinimumVersion, ReadyforUpgrade, VerdictSource, and FailureReason. Cluster names are folded into Target (rather than kept as their own column) for the ESX Host rows that carry one, to save a column in an already-wide report. .PARAMETER Context The VcfCheck.Context object. Must already be connected to SDDC Manager. .PARAMETER VcfDestinationRelease Either a full concrete VCF release (e.g. '9.1.0.0300'), a release family - major.minor. patch only, e.g. '9.1.0' - or 'latest'. A family or 'latest' is resolved independently per component (SDDC Manager, vCenter, ESX, NSX) to that component's own newest published release matching the request, via Resolve-VcfCheckInteropMatrixReleaseInFamily - two components in the same release train can ship a different newest 4th-digit patch (e.g. NSX 9.1.0.0300 while vCenter is still at 9.1.0.0100), so resolving once centrally would misrepresent at least one of them. A full concrete release skips resolution entirely and is used exactly as given, unchanged from this parameter's original behavior. Defaults to $Script:VcfCheckDefaultDestinationRelease ('latest'). .PARAMETER DisplayName Optional friendly display name for the check result. .OUTPUTS [PSObject] A single VcfCheck.Result object. #> [CmdletBinding()] [OutputType([PSObject])] Param ( [Parameter(Mandatory = $true)] [PSObject]$Context, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$VcfDestinationRelease = $Script:VcfCheckDefaultDestinationRelease, [Parameter(Mandatory = $false)] [String]$DisplayName = '' ) $startedAt = Get-Date $checkId = 'sddc_bom_check' $catalogEntry = (Get-VcfCheckCatalog)[$checkId] $displayName = if ([String]::IsNullOrEmpty($DisplayName)) { $catalogEntry.displayName } else { $DisplayName } $legacyMinVersions = @{ 'SDDC_MANAGER' = '5.2' 'VCENTER' = '8.0.3' 'ESX' = '8.0.3' 'NSX_MANAGER' = '4.2' 'NSX_EDGE' = '4.2' } $isDestinationReleaseFamily = ($VcfDestinationRelease -eq 'latest') -or ($VcfDestinationRelease -match '^\d+\.\d+\.\d+$') $destinationVersions = @{} foreach ($interopComponent in @('SDDC_MANAGER', 'VCENTER', 'ESX', 'NSX')) { $destinationVersions[$interopComponent] = $VcfDestinationRelease if ($isDestinationReleaseFamily) { $resolvedRelease = Resolve-VcfCheckInteropMatrixReleaseInFamily -Component $interopComponent -Family $VcfDestinationRelease if ($resolvedRelease) { $destinationVersions[$interopComponent] = $resolvedRelease } } } $bomResolution = Resolve-VcfCheckBomDestinationThreshold -VcfDestinationRelease $destinationVersions['SDDC_MANAGER'] -LegacyMinVersions $legacyMinVersions $minVersions = $bomResolution.MinVersions $rows = [System.Collections.Generic.List[PSCustomObject]]::new() $nonCompliant = [System.Collections.Generic.List[String]]::new() $unconfirmedNonCompliant = [System.Collections.Generic.List[String]]::new() try { $domains = @((Invoke-VcfGetDomains -ErrorAction Stop).Elements) $mgmtDomain = $domains | Where-Object { $_.Type -eq 'MANAGEMENT' } | Select-Object -First 1 $mgmtDomainName = if ($mgmtDomain -and $mgmtDomain.Name) { $mgmtDomain.Name } else { 'Management' } # ---------------------------------------------------- # 1. SDDC Manager Check # ---------------------------------------------------- foreach ($sddc in @((Invoke-VcfGetSddcManagers -ErrorAction Stop).Elements)) { $targetName = if ($sddc.Fqdn) { $sddc.Fqdn } else { $Context.SddcManagerFqdn } $row = New-VcfCheckBomComplianceRow -Domain $mgmtDomainName -Component 'SDDC Manager' ` -Target $targetName -InstalledVersion $sddc.Version -MinimumVersion $minVersions['SDDC_MANAGER'] ` -InteropComponent 'SDDC_MANAGER' -DestinationVersion $destinationVersions['SDDC_MANAGER'] $rows.Add($row) Add-VcfCheckBomComplianceMessage -Row $row -Label "SDDC Manager (${targetName})" ` -NonCompliant $nonCompliant -UnconfirmedNonCompliant $unconfirmedNonCompliant } # ---------------------------------------------------- # 2. Iterate All VCF Domains # ---------------------------------------------------- foreach ($domain in $domains) { $domainName = $domain.Name $domainId = $domain.Id # --- Domain vCenters --- foreach ($vc in @((Invoke-VcfGetVcenters -DomainId $domainId -ErrorAction Stop).Elements)) { $row = New-VcfCheckBomComplianceRow -Domain $domainName -Component 'vCenter' ` -Target $vc.Fqdn -InstalledVersion $vc.Version -MinimumVersion $minVersions['VCENTER'] ` -InteropComponent 'VCENTER' -DestinationVersion $destinationVersions['VCENTER'] $rows.Add($row) Add-VcfCheckBomComplianceMessage -Row $row -Label "Domain [$domainName] vCenter ($($vc.Fqdn))" ` -NonCompliant $nonCompliant -UnconfirmedNonCompliant $unconfirmedNonCompliant } # --- ESX Hosts, Grouped by Cluster --- $hostsByCluster = Get-VcfCheckHostsGroupedByCluster -DomainId $domainId foreach ($clusterName in $hostsByCluster.Keys) { $clusterHosts = $hostsByCluster[$clusterName] $uniqueVersions = @($clusterHosts | Select-Object -ExpandProperty EsxiVersion -Unique) if ($uniqueVersions.Count -eq 1) { # All hosts in this cluster share the same version - report the cluster as one row. $targetText = "Cluster [$clusterName]: All $($clusterHosts.Count) hosts have the same version" $row = New-VcfCheckBomComplianceRow -Domain $domainName -Component 'ESX Host' ` -Target $targetText -InstalledVersion $uniqueVersions[0] -MinimumVersion $minVersions['ESX'] ` -InteropComponent 'ESX' -DestinationVersion $destinationVersions['ESX'] $rows.Add($row) Add-VcfCheckBomComplianceMessage -Row $row -Label "Domain [$domainName] $targetText" ` -NonCompliant $nonCompliant -UnconfirmedNonCompliant $unconfirmedNonCompliant } else { # Version drift detected in the cluster - expand each host individually. foreach ($hostObj in $clusterHosts) { $targetText = "Cluster [$clusterName]: $($hostObj.Fqdn)" $row = New-VcfCheckBomComplianceRow -Domain $domainName -Component 'ESX Host' ` -Target $targetText -InstalledVersion $hostObj.EsxiVersion -MinimumVersion $minVersions['ESX'] ` -InteropComponent 'ESX' -DestinationVersion $destinationVersions['ESX'] $rows.Add($row) Add-VcfCheckBomComplianceMessage -Row $row -Label "Domain [$domainName] $targetText" ` -NonCompliant $nonCompliant -UnconfirmedNonCompliant $unconfirmedNonCompliant } } } # --- NSX Components Check --- $nsxResources = $null try { $nsxResources = Invoke-VcfGetNsxUpgradeResources -DomainId $domainId -ErrorAction Stop } catch { # Safeguard if domain NSX resources cannot be queried Write-LogMessage -Type WARNING -Message "Could not query NSX upgrade resources for domain $domainId`: $($_.Exception.Message)" } if ($nsxResources) { if ($nsxResources.NsxtManagerCluster) { $targetName = if ($nsxResources.NsxtManagerCluster.Name) { $nsxResources.NsxtManagerCluster.Name } else { "NSX-Manager-$domainName" } $row = New-VcfCheckBomComplianceRow -Domain $domainName -Component 'NSX Manager' ` -Target $targetName -InstalledVersion $nsxResources.NsxtManagerCluster.Version -MinimumVersion $minVersions['NSX_MANAGER'] ` -InteropComponent 'NSX' -DestinationVersion $destinationVersions['NSX'] $rows.Add($row) Add-VcfCheckBomComplianceMessage -Row $row -Label "Domain [$domainName] NSX Manager (${targetName})" ` -NonCompliant $nonCompliant -UnconfirmedNonCompliant $unconfirmedNonCompliant } foreach ($edgeCluster in @($nsxResources.NsxtEdgeClusters)) { $row = New-VcfCheckBomComplianceRow -Domain $domainName -Component 'NSX Edge Cluster' ` -Target $edgeCluster.Name -InstalledVersion $edgeCluster.Version -MinimumVersion $minVersions['NSX_EDGE'] ` -InteropComponent 'NSX' -DestinationVersion $destinationVersions['NSX'] $rows.Add($row) Add-VcfCheckBomComplianceMessage -Row $row -Label "Domain [$domainName] Edge Cluster ($($edgeCluster.Name))" ` -NonCompliant $nonCompliant -UnconfirmedNonCompliant $unconfirmedNonCompliant } } } } catch { return New-VcfCheckResult -CheckId $checkId -Status Error ` -TargetComponent $Context.SddcManagerFqdn -Exception $_.Exception.Message ` -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $displayName } if ($nonCompliant.Count -gt 0) { $status = 'Fail' $detail = "Not ready to upgrade to VCF $VcfDestinationRelease`: $($nonCompliant -join '; ')" } elseif ($unconfirmedNonCompliant.Count -gt 0) { $status = 'Warning' $detail = "Below the minimum version floor for VCF $VcfDestinationRelease, but Broadcom's Interop Matrix has no data yet for this exact pair - unconfirmed, not a blocking failure: $($unconfirmedNonCompliant -join '; ')" } else { $status = 'Pass' $detail = "SDDC Manager, vCenters, ESX host clusters, and NSX resources across all domains meet the minimum versions required for VCF $VcfDestinationRelease." } if (-not [String]::IsNullOrWhiteSpace($bomResolution.Note)) { $detail += " $($bomResolution.Note)" } $resultParams = @{ CheckId = $checkId Status = $status TargetComponent = $Context.SddcManagerFqdn Detail = $detail Rows = $rows.ToArray() StartedAt = $startedAt CompletedAt = (Get-Date) } if ($status -eq 'Fail') { $resultParams['Blocking'] = $true } elseif ($status -eq 'Warning') { $resultParams['Blocking'] = $false } return New-VcfCheckResult @resultParams -DisplayName $displayName } function Add-VcfCheckBomComplianceMessage { <# .SYNOPSIS Routes one non-compliant Test-VcfSddcBomCheck row's formatted message into the confirmed or unconfirmed non-compliance list, based on which source decided its verdict. .DESCRIPTION A row whose VerdictSource is 'Interop Matrix' is a real, confirmed non-compliance - it goes into NonCompliant and blocks the check. A row that only missed the MinimumVersion floor (VerdictSource 'Minimum Version Floor') goes into UnconfirmedNonCompliant instead, since the shipped Interop Matrix snapshot may simply not have data for this pair yet - see New-VcfCheckBomComplianceRow's InteropComponent parameter notes. A compliant row is a no-op. .PARAMETER Row One New-VcfCheckBomComplianceRow result. .PARAMETER Label Human-readable identifier for the row's target, forwarded to Format-VcfCheckBomNonCompliantMessage. .PARAMETER NonCompliant The check's confirmed non-compliance message list. .PARAMETER UnconfirmedNonCompliant The check's unconfirmed (floor-only) non-compliance message list. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [PSObject]$Row, [Parameter(Mandatory = $true)] [String]$Label, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.List[String]]$NonCompliant, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.List[String]]$UnconfirmedNonCompliant ) if ($Row.ReadyforUpgrade) { return } $message = Format-VcfCheckBomNonCompliantMessage -Row $Row -Label $Label if ($Row.VerdictSource -eq 'Interop Matrix') { $NonCompliant.Add($message) } else { $UnconfirmedNonCompliant.Add($message) } } function Format-VcfCheckBomNonCompliantMessage { <# .SYNOPSIS Formats one Test-VcfSddcBomCheck non-compliant component's call to action, worded differently depending on why it's non-compliant. .DESCRIPTION A component below its minimum version just needs upgrading. A component the Interop Matrix itself flags Incompatible (FailureReason starting with 'Incompatible') is a different, more important case: the installed version may already clear any numeric floor, but no published VCF release combination supports it yet (e.g. a release-date-based exception - see New-VcfCheckBomComplianceRow's .NOTES). Upgrading that component alone would not fix this - the destination VCF release itself needs to move forward, so the call to action is to wait for a newer VCF 9.x release rather than to change the component. .PARAMETER Row One New-VcfCheckBomComplianceRow result, already confirmed non-compliant. .PARAMETER Label Human-readable identifier for the row's target, e.g. 'SDDC Manager (vcf01.example.com)'. .OUTPUTS [String] one ready-to-join sentence for the check's Detail message. #> [CmdletBinding()] [OutputType([String])] Param ( [Parameter(Mandatory = $true)] [PSObject]$Row, [Parameter(Mandatory = $true)] [String]$Label ) if ($Row.FailureReason -like 'Incompatible*' -or $Row.FailureReason -like 'Not Supported*') { return "$Label (Installed: $($Row.InstalledVersion)) - $($Row.FailureReason) - wait for a newer VCF 9.x release before upgrading" } return "$Label (Installed: $($Row.InstalledVersion), Required: $($Row.MinimumVersion))" } function Resolve-VcfCheckBomDestinationThreshold { <# .SYNOPSIS Resolves per-component minimum-version thresholds for Test-VcfSddcBomCheck's chosen destination VCF release. .DESCRIPTION Tries Get-VcfCheckReleaseBom first - the connected SDDC Manager's own release catalog, exact and in SDDC Manager's own version-string format. Falls back to LegacyMinVersions (default floor thresholds) only when that catalog has no entry for the requested release, in which case it also asks Broadcom's public Interop Matrix (SDDC Manager product ID 851) whether the requested release even exists, purely so an environment falling back silently still gets a diagnostic note distinguishing "SDDC Manager just hasn't synced this real release yet" from "this release string isn't real." That confirmatory call is best-effort: no internet access or an API failure still returns the default thresholds, just with a note that the release could not be confirmed either way - never as a compliance failure. .PARAMETER VcfDestinationRelease The VCF release requested by the check's caller, e.g. '9.1.0.0'. .PARAMETER LegacyMinVersions Hashtable of component-type -> minimum version to fall back to when no live BOM is found. .OUTPUTS [PSObject] with MinVersions (Hashtable, same shape as LegacyMinVersions) and Note (String) describing which source was used. #> [CmdletBinding()] [OutputType([PSObject])] Param ( [Parameter(Mandatory = $true)] [String]$VcfDestinationRelease, [Parameter(Mandatory = $true)] [Hashtable]$LegacyMinVersions ) $bom = $null try { $bom = Get-VcfCheckReleaseBom -Version $VcfDestinationRelease -ErrorAction Stop } catch { Write-LogMessage -Type WARNING -Message "Could not resolve VCF $VcfDestinationRelease from SDDC Manager's release catalog: $($_.Exception.Message)" } if ($bom -and $bom.Count -gt 0) { $nsxManagerVersion = Find-VcfCheckBomVersionForComponent -Bom $bom -ComponentType 'NSX_T_MANAGER' $minVersions = @{ 'SDDC_MANAGER' = Find-VcfCheckBomVersionForComponent -Bom $bom -ComponentType 'SDDC_MANAGER' 'VCENTER' = Find-VcfCheckBomVersionForComponent -Bom $bom -ComponentType 'VCENTER' 'ESX' = Find-VcfCheckBomVersionForComponent -Bom $bom -ComponentType 'ESX_HOST' 'NSX_MANAGER' = $nsxManagerVersion 'NSX_EDGE' = $nsxManagerVersion } $unresolvedComponents = @($minVersions.Keys | Where-Object { [String]::IsNullOrWhiteSpace($minVersions[$_]) }) foreach ($componentKey in $unresolvedComponents) { $minVersions[$componentKey] = $LegacyMinVersions[$componentKey] } # A clean resolution needs no note - only the fallback case below and an unresolved # component are actionable enough to surface in the check's Detail message. $note = if ($unresolvedComponents.Count -gt 0) { "Could not match a BOM entry for: $($unresolvedComponents -join ', '); used default fallback thresholds for those." } else { '' } return [PSCustomObject]@{ MinVersions = $minVersions; Note = $note } } $publishedCheck = $null try { $publishedCheck = Test-VcfCheckInteropMatrixVersionPublished -ProductId 851 -Version $VcfDestinationRelease } catch { Write-LogMessage -Type WARNING -Message "Interop Matrix release-existence check failed for VCF $VcfDestinationRelease`: $($_.Exception.Message)" } # $true/unconfirmable are both expected, non-actionable outcomes of a normal default-threshold # fallback (an offline SDDC Manager catalog, or no internet access) - only a confirmed-false # result (a fictitious/typo'd version string) is worth surfacing to the user. $note = "SDDC Manager's release catalog has no entry for VCF $VcfDestinationRelease; used default minimum-version thresholds instead." if ($publishedCheck -eq $false) { $note += " Broadcom's Interop Matrix does not list this as a published release - double-check the version string." } return [PSCustomObject]@{ MinVersions = $LegacyMinVersions; Note = $note } } function New-VcfCheckBomComplianceRow { <# .SYNOPSIS Parses one component's installed version against its minimum threshold and builds its Test-VcfSddcBomCheck Rows entry. .DESCRIPTION Shared by every component type Test-VcfSddcBomCheck evaluates (SDDC Manager, vCenter, ESX Host, NSX Manager, NSX Edge Cluster) so the parse-compare-build sequence lives in exactly one place instead of being repeated per component. .PARAMETER Domain Domain name to stamp on the row (or 'N/A' for a single-target component). .PARAMETER Component Human-readable component label, e.g. 'SDDC Manager', 'ESX Host'. .PARAMETER Target The specific target this row describes (an FQDN, or a summary string for a cluster-wide row such as "Cluster [m01-cl01]: All 4 hosts have the same version" - the cluster name is folded into Target by the caller rather than carried as its own row property, to save a column in an already-wide report). .PARAMETER InstalledVersion Raw version string as reported by the component. May be $null/empty if the component never reported one - ConvertTo-VcfCheckSimpleVersion treats that as non-compliant. .PARAMETER MinimumVersion Minimum required version string from Test-VcfSddcBomCheck's threshold map - either a fallback floor (e.g. '8.0.3') or a live BOM entry that may carry a build suffix (e.g. '9.0.0.0-24755230'); parsed the same way as InstalledVersion before comparing. Only used as a fallback when InteropComponent/DestinationVersion don't produce a real verdict. .PARAMETER InteropComponent 'SDDC_MANAGER', 'VCENTER', 'ESX', 'NSX', 'VRA', 'VROPS', 'VRNI', 'VRO', 'VRSLCM', 'VRLI', or 'VIDM' - when supplied along with DestinationVersion, the row's Compliant status is primarily decided by Broadcom's public Interop Matrix's own real per-version-pair upgrade-path status (Get-VcfCheckInteropMatrixCompatibilityVerdict) rather than a floor comparison, since a floor only approximates compatibility and misses real exceptions (e.g. a release-date-based one - see this function's own .NOTES). Falls back to the MinimumVersion floor comparison when the shipped Interop Matrix snapshot has no resolvable verdict for this exact pair (unresolvable row, or pair not listed) - that fallback is a best-effort estimate, not a confirmed verdict, since the shipped snapshot (Data/Interoperability/*.json) is refreshed manually and can lag behind a newly-published release; the caller uses VerdictSource to avoid blocking an upgrade on an unconfirmed floor miss the way it would on a real Interop Matrix Incompatible result. .PARAMETER DestinationVersion The destination VCF release version (e.g. '9.1.0.0'), required alongside InteropComponent for the shipped Interop Matrix snapshot lookup. .OUTPUTS [PSObject] one Rows entry: Domain, Component, Target, InstalledVersion, MinimumVersion, ReadyforUpgrade, VerdictSource ('Interop Matrix' when a real per-pair verdict was found, 'Minimum Version Floor' when it fell back to the floor comparison), FailureReason (from Get-VcfCheckInteropMatrixFailureReason when the Interop Matrix path decided the verdict; a minimum-version-floor message otherwise; 'N/A' when ReadyforUpgrade is true). When VerdictSource is 'Interop Matrix', MinimumVersion is recomputed on the fly via Get-VcfCheckInteropMatrixMinimumCompatibleVersion (the lowest source version the shipped snapshot marks Compatible for DestinationVersion) rather than echoing back the passed-in floor, which played no part in the verdict. Falls back to 'N/A (Interop Matrix per-pair verdict)' only if that lookup itself returns nothing. .NOTES CONFIRMED live, 2026-08-18: ESX build 25595708 ("8.0U3k") reports Incompatible against VCF ESX release 9.1.0.0100 via the Interop Matrix (a release-date-based exception - 8.0U3k's own release date is later than 9.1.0.0100's), while a floor comparison alone would have called it compliant (8.0.3 >= 8.0.3 floor, or even against a 9.x floor if this were a newer ESX build). This is why InteropComponent/DestinationVersion take priority over MinimumVersion whenever a real verdict is available. #> [CmdletBinding()] [OutputType([PSObject])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds and returns a PSCustomObject Rows entry - no system state is changed despite the New- verb.')] Param ( [Parameter(Mandatory = $true)] [String]$Domain, [Parameter(Mandatory = $true)] [String]$Component, [Parameter(Mandatory = $true)] [AllowEmptyString()] [String]$Target, [Parameter(Mandatory = $false)] [AllowNull()] [AllowEmptyString()] [String]$InstalledVersion, [Parameter(Mandatory = $true)] [String]$MinimumVersion, [Parameter(Mandatory = $false)] [ValidateSet('SDDC_MANAGER', 'VCENTER', 'ESX', 'NSX', 'VRA', 'VROPS', 'VRNI', 'VRO', 'VRSLCM', 'VRLI', 'VIDM')] [String]$InteropComponent, [Parameter(Mandatory = $false)] [AllowEmptyString()] [String]$DestinationVersion ) $checkedAgainst = 'Minimum Version Floor' $isCompliant = $null $askedInteropMatrix = $InteropComponent -and -not [String]::IsNullOrWhiteSpace($DestinationVersion) -and -not [String]::IsNullOrWhiteSpace($InstalledVersion) if ($askedInteropMatrix) { $verdict = $null try { $verdict = Get-VcfCheckInteropMatrixCompatibilityVerdict -Component $InteropComponent -InstalledVersion $InstalledVersion -DestinationVersion $DestinationVersion -ErrorAction Stop } catch { Write-LogMessage -Type WARNING -Message "Interop Matrix compatibility lookup failed for $Component ($InstalledVersion -> $DestinationVersion): $($_.Exception.Message)" } if ($verdict) { $checkedAgainst = 'Interop Matrix' $isCompliant = ($verdict -eq 'Compatible') } } if ($null -eq $isCompliant) { $parsedActual = ConvertTo-VcfCheckSimpleVersion -VersionString $InstalledVersion $parsedRequired = ConvertTo-VcfCheckSimpleVersion -VersionString $MinimumVersion $isCompliant = ($null -ne $parsedActual -and $null -ne $parsedRequired -and $parsedActual -ge $parsedRequired) } $failureReason = 'N/A' if (-not $isCompliant) { if ($checkedAgainst -eq 'Interop Matrix') { $failureReason = Get-VcfCheckInteropMatrixFailureReason -Component $InteropComponent -InstalledVersion $InstalledVersion -DestinationVersion $DestinationVersion if (-not $failureReason) { $failureReason = "Incompatible - flagged by Broadcom's Interop Matrix, wait for a newer VCF 9.x release before upgrading" } } else { $failureReason = "Below Minimum Version Floor, unconfirmed - no Interop Matrix data for this pair (Installed: $InstalledVersion, Required: $MinimumVersion)" } } $displayedMinimumVersion = $MinimumVersion if ($checkedAgainst -eq 'Interop Matrix') { $interopFloor = $null try { $interopFloor = Get-VcfCheckInteropMatrixMinimumCompatibleVersion -Component $InteropComponent -DestinationVersion $DestinationVersion -ErrorAction Stop } catch { Write-LogMessage -Type WARNING -Message "Interop Matrix floor lookup failed for $Component (-> $DestinationVersion): $($_.Exception.Message)" } $displayedMinimumVersion = if ($interopFloor) { $interopFloor } else { 'N/A (Interop Matrix per-pair verdict)' } } return [PSCustomObject]@{ Domain = $Domain Component = $Component Target = $Target InstalledVersion = $InstalledVersion MinimumVersion = $displayedMinimumVersion ReadyforUpgrade = $isCompliant VerdictSource = $checkedAgainst FailureReason = $failureReason } } function ConvertTo-VcfCheckSimpleVersion { <# .SYNOPSIS Parses a VCF component version string into a comparable [Version]. .DESCRIPTION VCF component version strings mix a marketing version with a numeric build suffix (e.g. "8.0.3.00600-24853646", "4.2.0.0.0.24105817"). [Version] only supports up to four numeric parts (Major.Minor.Build.Revision) and throws on anything else, so this strips a trailing "-<build>" suffix, then collects numeric groups left-to-right until either four groups are collected or a 7+ digit group is seen - that length reliably identifies a trailing build/changeset number rather than a version segment, confirmed against real VCF component version strings. Pads with zeros up to all four [Version] parts so that differently-truncated strings for the same release (e.g. Interop Matrix "5.2.2" versus an installed build's "5.2.2.0") normalize to the same [Version] value and compare equal. .PARAMETER VersionString Raw version string as reported by a VCF component (SDDC Manager, vCenter, ESX host, NSX Manager/Edge). .OUTPUTS [Version], or $null if the input was empty or had no leading numeric version information - lets a caller treat "could not determine a version" as simply non-compliant instead of wrapping every call site in its own try/catch. .EXAMPLE ConvertTo-VcfCheckSimpleVersion -VersionString '8.0.3.00600-24853646' # 8.0.3.600 #> [CmdletBinding()] [OutputType([Version])] Param ( [Parameter(Mandatory = $false)] [AllowNull()] [AllowEmptyString()] [String]$VersionString ) if ([String]::IsNullOrWhiteSpace($VersionString)) { return $null } $clean = ($VersionString -replace '-.*$', '').Trim() $regexMatches = [Regex]::Matches($clean, '\d+') if ($regexMatches.Count -eq 0) { return $null } $validParts = [System.Collections.Generic.List[Int]]::new() foreach ($match in $regexMatches) { if ($match.Value.Length -ge 7) { break } $validParts.Add([Int]$match.Value) if ($validParts.Count -eq 4) { break } } while ($validParts.Count -lt 4) { $validParts.Add(0) } try { return [Version]($validParts -join '.') } catch { return $null } } function Get-VcfCheckHostsGroupedByCluster { <# .SYNOPSIS Resolves and groups a domain's ESX hosts by cluster name, for Test-VcfSddcBomCheck. .DESCRIPTION Invoke-VcfGetHosts's own Host elements don't reliably carry a resolved cluster name across every VCF version, so this cross-references Invoke-VcfGetClusters's Id->Name map and per-cluster Hosts membership list as a fallback, trying (in order): the cluster membership list keyed by the host's Fqdn/Id, then the host's own Cluster.Name/ ClusterName, then the cluster Id map keyed by the host's ClusterId/Cluster.Id. A host whose cluster cannot be resolved by any of these is grouped under 'Unassigned' rather than dropped, so a version-drift issue on an otherwise-unmapped host still surfaces in the report. .PARAMETER DomainId The VCF domain Id to query hosts and clusters for. .OUTPUTS [System.Collections.Specialized.OrderedDictionary] cluster name -> List[PSObject] of that cluster's ESX host objects (Invoke-VcfGetHosts's own Host elements). #> [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] Param ( [Parameter(Mandatory = $true)] [String]$DomainId ) $clusterIdMap = @{} $hostClusterMap = @{} try { foreach ($cluster in @((Invoke-VcfGetClusters -DomainId $DomainId -ErrorAction Stop).Elements)) { if ($cluster.Id -and $cluster.Name) { $clusterIdMap[$cluster.Id] = $cluster.Name } foreach ($clusterHost in @($cluster.Hosts)) { if ($clusterHost.Fqdn) { $hostClusterMap[$clusterHost.Fqdn] = $cluster.Name } if ($clusterHost.Id) { $hostClusterMap[$clusterHost.Id] = $cluster.Name } if ($clusterHost.HostName) { $hostClusterMap[$clusterHost.HostName] = $cluster.Name } } } } catch { # A domain whose cluster query fails still reports its hosts below, grouped as 'Unassigned'. Write-LogMessage -Type WARNING -Message "Could not resolve cluster membership for domain $DomainId`: $($_.Exception.Message)" } $hostsByCluster = [Ordered]@{} foreach ($hostObj in @((Invoke-VcfGetHosts -DomainId $DomainId -ErrorAction Stop).Elements)) { $clusterName = switch ($true) { { $hostObj.Fqdn -and $hostClusterMap.ContainsKey($hostObj.Fqdn) } { $hostClusterMap[$hostObj.Fqdn]; break } { $hostObj.Id -and $hostClusterMap.ContainsKey($hostObj.Id) } { $hostClusterMap[$hostObj.Id]; break } { $hostObj.Cluster.Name } { $hostObj.Cluster.Name; break } { $hostObj.ClusterName } { $hostObj.ClusterName; break } { $hostObj.ClusterId -and $clusterIdMap.ContainsKey($hostObj.ClusterId) } { $clusterIdMap[$hostObj.ClusterId]; break } { $hostObj.Cluster.Id -and $clusterIdMap.ContainsKey($hostObj.Cluster.Id) } { $clusterIdMap[$hostObj.Cluster.Id]; break } default { 'Unassigned' } } if ([String]::IsNullOrWhiteSpace($clusterName)) { $clusterName = 'Unassigned' } if (-not $hostsByCluster.Contains($clusterName)) { $hostsByCluster[$clusterName] = [System.Collections.Generic.List[PSCustomObject]]::new() } $hostsByCluster[$clusterName].Add($hostObj) } return $hostsByCluster } |