Private/Checks/SddcManager/Test-VcfSddcVersionAliasCheck.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-VcfSddcVersionAliasCheck { <# .SYNOPSIS Validates that every vCenter, ESX host, and NSX Manager across all VCF domains either matches the current release's declared BOM version directly, or is covered by a registered LCM version alias. .DESCRIPTION Queries SDDC Manager domains and verifies that vCenter, ESX host, and NSX Manager components match their domain's current release Bill of Materials (BOM) version or are covered by a registered LCM version alias. When async patches or hotfixes are applied to components, their exact version string may differ from the base BOM version. Registering version aliases in SDDC Manager maps these patch versions to expected base releases, preventing validation failures during lifecycle operations. Evaluates every host in every domain: - ESX hosts within a cluster sharing the same version and alias coverage are rolled up into a single cluster row. - Clusters experiencing version drift expand to individual per-host rows. Resolves domain current release BOM via Invoke-VcfGetReleases and alias mappings via Invoke-VcfGetVersionAliasConfiguration. Evaluates coverage and populates a structured Rows table detailing Domain, Cluster, Component, Target, InstalledVersion, ExpectedBomVersion, AliasApplied, Covered, and Note. .PARAMETER Context The VcfCheck.Context object. Must already be connected to SDDC Manager. .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)] [String]$DisplayName = '' ) $startedAt = Get-Date $checkId = 'sddc_version_alias_check' $catalogEntry = (Get-VcfCheckCatalog)[$checkId] $displayName = if ([String]::IsNullOrEmpty($DisplayName)) { $catalogEntry.displayName } else { $DisplayName } $rows = [System.Collections.Generic.List[PSCustomObject]]::new() $uncoveredCount = 0 $domainsMissingBom = [System.Collections.Generic.List[String]]::new() try { $domains = @((Invoke-VcfGetDomains -ErrorAction Stop).Elements) $aliasMap = Get-VcfCheckVersionAliasMap foreach ($domain in $domains) { $domainName = $domain.Name $domainId = $domain.Id $currentBom = Get-VcfCheckDomainCurrentBom -DomainId $domainId if (-not $currentBom -or $currentBom.Count -eq 0) { $domainsMissingBom.Add($domainName) continue } $bomComponentNames = @($currentBom.Keys) foreach ($vc in @((Invoke-VcfGetVcenters -DomainId $domainId -ErrorAction Stop).Elements)) { $bomVersion = Find-VcfCheckBomVersionForComponent -Bom $currentBom -ComponentType 'VCENTER' $row = New-VcfCheckVersionAliasRow -Domain $domainName -Cluster 'N/A' -Component 'vCenter' -ComponentType 'VCENTER' ` -Target $vc.Fqdn -InstalledVersion $vc.Version -BomVersion $bomVersion -BomComponentNames $bomComponentNames -AliasMap $aliasMap $rows.Add($row) if (-not $row.Covered) { $uncoveredCount++ } } $bomVersion = Find-VcfCheckBomVersionForComponent -Bom $currentBom -ComponentType 'ESX_HOST' $hostsByCluster = Get-VcfCheckHostsGroupedByCluster -DomainId $domainId foreach ($clusterName in ($hostsByCluster.Keys | Sort-Object)) { $clusterRows = @(New-VcfCheckVersionAliasClusterRowSet -Domain $domainName -Cluster $clusterName ` -ClusterHosts $hostsByCluster[$clusterName] -BomVersion $bomVersion -BomComponentNames $bomComponentNames -AliasMap $aliasMap) foreach ($clusterRow in $clusterRows) { $rows.Add($clusterRow) if (-not $clusterRow.Covered) { $uncoveredCount++ } } } $nsxResources = $null try { $nsxResources = Invoke-VcfGetNsxUpgradeResources -DomainId $domainId -ErrorAction Stop } catch { # Safeguard if domain NSX resources cannot be queried - vCenter/ESX rows still stand. } if ($nsxResources -and $nsxResources.NsxtManagerCluster) { $targetName = if ($nsxResources.NsxtManagerCluster.Name) { $nsxResources.NsxtManagerCluster.Name } else { "NSX-Manager-$domainName" } $bomVersion = Find-VcfCheckBomVersionForComponent -Bom $currentBom -ComponentType 'NSX_T_MANAGER' $row = New-VcfCheckVersionAliasRow -Domain $domainName -Cluster 'N/A' -Component 'NSX Manager' -ComponentType 'NSX_T_MANAGER' ` -Target $targetName -InstalledVersion $nsxResources.NsxtManagerCluster.Version -BomVersion $bomVersion -BomComponentNames $bomComponentNames -AliasMap $aliasMap $rows.Add($row) if (-not $row.Covered) { $uncoveredCount++ } } } } catch { return New-VcfCheckResult -CheckId $checkId -Status Error ` -TargetComponent $Context.SddcManagerFqdn -Exception $_.Exception.Message ` -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $displayName } if ($uncoveredCount -gt 0) { $status = 'Fail' $detail = "$uncoveredCount of $($rows.Count) evaluated component(s) are running a build that does not match the current release BOM and is not covered by a registered version alias. See the Rows table for details." } else { $status = 'Pass' $detail = "All $($rows.Count) evaluated component(s) across all domains are covered." } if ($domainsMissingBom.Count -gt 0) { $detail += " No current-release BOM could be resolved for domain(s): $($domainsMissingBom -join ', '); those domains were skipped." } $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 } return New-VcfCheckResult @resultParams -DisplayName $displayName } function New-VcfCheckVersionAliasClusterRowSet { <# .SYNOPSIS Builds the ESX Host Rows entries for one cluster, rolled up to a single row when every host in the cluster is covered and shares the same version. .DESCRIPTION Reports one aggregated row for the cluster when all hosts share the same EsxiVersion and coverage outcome. Expands to individual host rows when version drift is detected within the cluster. .PARAMETER Domain Domain name to stamp on each row. .PARAMETER Cluster Cluster name to stamp on each row. .PARAMETER ClusterHosts The cluster's host objects (Invoke-VcfGetHosts elements, from Get-VcfCheckHostsGroupedByCluster). .PARAMETER BomVersion The current release's declared ESX version for this domain, or $null if unresolved. .PARAMETER BomComponentNames The current release BOM's actual component name keys, passed straight through to New-VcfCheckVersionAliasRow for its diagnostic Note when BomVersion is $null. .PARAMETER AliasMap Hashtable from Get-VcfCheckVersionAliasMap. .OUTPUTS [PSObject[]] one aggregated row, or one row per host. #> [CmdletBinding()] [OutputType([PSObject[]])] Param ( [Parameter(Mandatory = $true)] [String]$Domain, [Parameter(Mandatory = $true)] [String]$Cluster, [Parameter(Mandatory = $true)] [System.Collections.Generic.List[PSCustomObject]]$ClusterHosts, [Parameter(Mandatory = $false)] [AllowNull()] [AllowEmptyString()] [String]$BomVersion, [Parameter(Mandatory = $false)] [AllowNull()] [String[]]$BomComponentNames = @(), [Parameter(Mandatory = $true)] [Hashtable]$AliasMap ) $uniqueVersions = @($ClusterHosts | Select-Object -ExpandProperty EsxiVersion -Unique) if ($uniqueVersions.Count -eq 1) { $targetText = "All $($ClusterHosts.Count) hosts have the same version" return @(New-VcfCheckVersionAliasRow -Domain $Domain -Cluster $Cluster -Component 'ESX Host' -ComponentType 'ESX_HOST' ` -Target $targetText -InstalledVersion $uniqueVersions[0] -BomVersion $BomVersion -BomComponentNames $BomComponentNames -AliasMap $AliasMap) } return @($ClusterHosts | ForEach-Object { New-VcfCheckVersionAliasRow -Domain $Domain -Cluster $Cluster -Component 'ESX Host' -ComponentType 'ESX_HOST' ` -Target $_.Fqdn -InstalledVersion $_.EsxiVersion -BomVersion $BomVersion -BomComponentNames $BomComponentNames -AliasMap $AliasMap }) } function New-VcfCheckVersionAliasRow { <# .SYNOPSIS Parses one component's installed version against its current-release BOM version (falling back to a registered version alias) and builds its Test-VcfSddcVersionAliasCheck Rows entry. .DESCRIPTION Compares an installed component version against the expected BOM version. If the installed version does not match directly, checks for a registered version alias in AliasMap mapping the installed version to an expected base version. Populates row fields including Domain, Cluster, Component, Target, InstalledVersion, ExpectedBomVersion, AliasApplied, Covered, and diagnostic Note details. .PARAMETER Domain Domain name to stamp on the row. .PARAMETER Cluster Cluster name to stamp on the row, or 'N/A' for a component with no cluster association. .PARAMETER Component Human-readable component label, e.g. 'vCenter', 'ESX Host', 'NSX Manager'. .PARAMETER ComponentType The version-alias BundleComponentType code for this component (e.g. 'VCENTER'), used to look up any registered alias in AliasMap. .PARAMETER Target The specific target this row describes, e.g. an FQDN. .PARAMETER InstalledVersion Raw version string as reported by the component. May be $null/empty. .PARAMETER BomVersion The current release's declared version for this component, or $null if it could not be resolved from the domain's BOM. .PARAMETER BomComponentNames The current release BOM's actual component name keys (e.g. from Get-VcfCheckDomainCurrentBom's .Keys), used only to build a diagnostic Note when BomVersion is $null. .PARAMETER AliasMap Hashtable from Get-VcfCheckVersionAliasMap: ComponentType -> Hashtable of aliasVersion -> baseVersion. .OUTPUTS [PSObject] one Rows entry: Domain, Cluster, Component, Target, InstalledVersion, ExpectedBomVersion, AliasApplied, Covered, Note. #> [CmdletBinding()] [OutputType([PSObject])] Param ( [Parameter(Mandatory = $true)] [String]$Domain, [Parameter(Mandatory = $true)] [String]$Cluster, [Parameter(Mandatory = $true)] [String]$Component, [Parameter(Mandatory = $true)] [String]$ComponentType, [Parameter(Mandatory = $true)] [AllowEmptyString()] [String]$Target, [Parameter(Mandatory = $false)] [AllowNull()] [AllowEmptyString()] [String]$InstalledVersion, [Parameter(Mandatory = $false)] [AllowNull()] [AllowEmptyString()] [String]$BomVersion, [Parameter(Mandatory = $false)] [AllowNull()] [String[]]$BomComponentNames = @(), [Parameter(Mandatory = $true)] [Hashtable]$AliasMap ) $aliasApplied = $false $note = '' if ([String]::IsNullOrWhiteSpace($BomVersion)) { $covered = $false $note = 'Current release BOM has no declared version for this component.' if ($BomComponentNames -and $BomComponentNames.Count -gt 0) { $note += " Actual BOM component name(s) seen for this domain: $($BomComponentNames -join ', ')." } } elseif (Test-VcfCheckBomVersionMatch -Actual $InstalledVersion -Expected $BomVersion) { $covered = $true } else { $aliasedBase = $null if ($AliasMap.ContainsKey($ComponentType) -and -not [String]::IsNullOrWhiteSpace($InstalledVersion)) { $componentAliases = $AliasMap[$ComponentType] if ($componentAliases.ContainsKey($InstalledVersion.Trim())) { $aliasedBase = $componentAliases[$InstalledVersion.Trim()] } } if (-not [String]::IsNullOrWhiteSpace($aliasedBase) -and (Test-VcfCheckBomVersionMatch -Actual $aliasedBase -Expected $BomVersion)) { $covered = $true $aliasApplied = $true $note = "Covered by a version alias mapping to base version $aliasedBase." } else { $covered = $false $note = 'No registered version alias covers this build.' } } return [PSCustomObject]@{ Domain = $Domain Cluster = $Cluster Component = $Component Target = $Target InstalledVersion = $InstalledVersion ExpectedBomVersion = $BomVersion AliasApplied = $aliasApplied Covered = $covered Note = $note } } function Get-VcfCheckDomainCurrentBom { <# .SYNOPSIS Looks up the current, already-running release's BOM (component name -> version) for a domain. .DESCRIPTION Queries domain release details via Invoke-VcfGetReleases -DomainId to retrieve the active release and constructs a Hashtable mapping BOM component names to expected version strings. .PARAMETER DomainId The VCF domain Id to resolve the current release BOM for. .OUTPUTS [Hashtable] component name -> version string, or $null if the domain has no resolvable current release. #> [CmdletBinding()] [OutputType([Hashtable])] Param ( [Parameter(Mandatory = $true)] [String]$DomainId ) $releases = @((Invoke-VcfGetReleases -DomainId $DomainId -ErrorAction Stop).Elements) $release = $releases | Select-Object -First 1 if (-not $release) { return $null } $bom = @{} foreach ($entry in @($release.Bom)) { if ($entry -and -not [String]::IsNullOrWhiteSpace($entry.Name)) { $bom[$entry.Name] = $entry.Version } } return $bom } function Get-VcfCheckVersionAliasMap { <# .SYNOPSIS Builds a lookup of every registered LCM version alias, by bundle component type. .DESCRIPTION Queries registered LCM version aliases via Invoke-VcfGetVersionAliasConfiguration and flattens the result into a lookup Hashtable: BundleComponentType -> (aliasVersion -> baseVersion). .OUTPUTS [Hashtable] BundleComponentType -> Hashtable of aliasVersion -> baseVersion. Empty if no aliases are registered. #> [CmdletBinding()] [OutputType([Hashtable])] Param () $map = @{} $page = Invoke-VcfGetVersionAliasConfiguration -ErrorAction Stop foreach ($entry in @($page.Elements)) { if (-not $entry -or [String]::IsNullOrWhiteSpace($entry.BundleComponentType)) { continue } $componentMap = @{} foreach ($baseAlias in @($entry.VersionAliases)) { if (-not $baseAlias) { continue } foreach ($aliasVersion in @($baseAlias.Aliases)) { if (-not [String]::IsNullOrWhiteSpace($aliasVersion)) { $componentMap[$aliasVersion.Trim()] = $baseAlias.Version } } } $map[$entry.BundleComponentType] = $componentMap } return $map } function Find-VcfCheckBomVersionForComponent { <# .SYNOPSIS Resolves the current-release BOM's declared version for a version-alias component type. .DESCRIPTION Matches version-alias BundleComponentType codes (such as 'VCENTER', 'ESX_HOST', 'NSX_T_MANAGER', 'SDDC_MANAGER') against BOM component keys using candidate keywords. Accounts for varying component naming conventions (e.g., 'HOST' or 'ESX' for ESX hosts) and returns the matching BOM version string. .PARAMETER Bom Hashtable from Get-VcfCheckDomainCurrentBom: component name -> version string. .PARAMETER ComponentType The version-alias BundleComponentType code to resolve a BOM version for, e.g. 'VCENTER'. .OUTPUTS [String] the matched BOM version, or $null if no BOM entry name matched any of the component type's candidate keywords. #> [CmdletBinding()] [OutputType([String])] Param ( [Parameter(Mandatory = $true)] [AllowNull()] [Hashtable]$Bom, [Parameter(Mandatory = $true)] [String]$ComponentType ) if (-not $Bom -or $Bom.Count -eq 0) { return $null } $keywordsByComponentType = @{ 'SDDC_MANAGER' = @('SDDC_MANAGER', 'SDDC Manager') 'VCENTER' = @('VCENTER', 'vCenter') 'ESX_HOST' = @('HOST', 'ESX') 'NSX_T_MANAGER' = @('NSX_T_MANAGER', 'NSX') } $keywords = $keywordsByComponentType[$ComponentType] if (-not $keywords -or $keywords.Count -eq 0) { return $null } foreach ($keyword in $keywords) { $matchKey = $Bom.Keys | Where-Object { $_ -match [Regex]::Escape($keyword) } | Select-Object -First 1 if ($matchKey) { return $Bom[$matchKey] } } return $null } |