Private/Checks/Esx/Test-VcfEsxImageProfile.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-VcfEsxImageProfile { <# .SYNOPSIS Reports the ESX image/build string for every host across all vCenters managed by SDDC Manager. .DESCRIPTION Retrieves host inventory for each vCenter domain using Get-VcfCheckVMHostInventory and extracts the full ESX product/build string from each host's Summary.Config.Product.FullName API property, plus the applied image profile name, vendor, and acceptance level via Get-VcfCheckEsxImageProfileForHost (esxcli software profile get). The Summary.Config.Product build string is already available from inventory without an esxcli round trip, so when every host in a cluster reports the identical build string, the esxcli lookup runs against a single representative host for that cluster rather than every host - the same image profile applies to all of them in that case. Only a cluster with divergent build strings falls back to querying esxcli per host. The esxcli lookup is best-effort: a failure leaves ImageProfileName/Vendor/ AcceptanceLevel blank for the affected host(s) rather than failing the domain. Also queries each cluster's vLCM desired software specification via Get-VcfCheckClusterLcmSoftware (once per cluster, not per host) and adds the vendor Add-on, Components, and Firmware & Drivers Add-on (hardware support package) - the same breakdown shown on the vSphere Client's Cluster > Updates > Image page. Build string/image profile alone cannot distinguish two clusters running the identical base image with a different vendor Add-on or Firmware & Drivers Add-on applied. AddOn/Components/FirmwareAndDriversAddOn do not apply to a vLCM baseline-managed cluster, per Get-VcfCheckClusterImageBasedByMoRef, and report 'N/A' for those. This lookup is also best-effort: a failure on an image-managed cluster leaves AddOn/Components/FirmwareAndDriversAddOn as 'Unavailable' rather than failing the domain, and a field left unconfigured on the cluster (e.g. no vendor Add-on) reports 'None'. Informational only: builds a tabular breakdown containing Cluster, Hostname, Image, ImageProfileName, Vendor, AcceptanceLevel, AddOn, Components, and FirmwareAndDriversAddOn (sorted by Cluster, then Hostname). Reports 'Pass' per vCenter domain as long as host details are collected. Returns an 'Error' status for a domain if no hosts are found, if connection to vCenter fails, or if inventory retrieval fails. Iterates through all vCenters managed by SDDC Manager and delegates output generation to New-VcfCheckPerDomainResults to return results grouped per vCenter domain. Long-running on a domain with many clusters (the esxcli and vLCM software lookups per cluster dominate runtime): reports its own progress via Write-VcfCheckSubProgress as each cluster is processed. .PARAMETER Context The VcfCheck.Context object. Must already be connected to SDDC Manager. .PARAMETER DisplayName Optional friendly name for the check, used when returning top-level error results. .OUTPUTS [PSObject[]] Array of per-domain result objects generated by New-VcfCheckPerDomainResults. #> [CmdletBinding()] [OutputType([PSObject[]])] Param ( [Parameter(Mandatory = $true)] [PSObject]$Context, [Parameter(Mandatory = $false)] [String]$DisplayName = '' ) $startedAt = Get-Date $checkId = 'esx_image_profile' $catalogEntry = (Get-VcfCheckCatalog)[$checkId] $displayName = if ([String]::IsNullOrEmpty($DisplayName)) { $catalogEntry.displayName } else { $DisplayName } try { $vcenterFqdns = Get-VcfCheckAllVCenterFqdns -Context $Context } catch { return New-VcfCheckResult -CheckId $checkId -Status Error ` -Exception $_.Exception.Message -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $displayName } $isImageBasedByMoRef = try { Get-VcfCheckClusterImageBasedByMoRef } catch { @{} } $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.'; Rows = @() } } else { $hostsByCluster = @($hosts | ForEach-Object { [PSCustomObject]@{ Cluster = $_.Parent.Name ClusterId = $_.Parent.ExtensionData.MoRef.Value Hostname = $_.Name Image = $_.ExtensionData.Summary.Config.Product.FullName VMHost = $_ } } | Group-Object -Property Cluster) $rows = @() $clusterIndex = 0 foreach ($clusterGroup in $hostsByCluster) { $clusterIndex++ Write-VcfCheckSubProgress -Context $Context -Current $clusterIndex -Total $hostsByCluster.Count -Label $clusterGroup.Name -Unit 'clusters' $clusterName = $clusterGroup.Name $hostsInCluster = @($clusterGroup.Group) $uniqueImages = @($hostsInCluster | Select-Object -Property Image -Unique) if ($uniqueImages.Count -eq 1) { $imageProfile = try { Get-VcfCheckEsxImageProfileForHost -VMHost $hostsInCluster[0].VMHost } catch { $null } foreach ($hostRecord in $hostsInCluster) { $hostRecord | Add-Member -NotePropertyName ImageProfileName -NotePropertyValue $imageProfile.Name -Force $hostRecord | Add-Member -NotePropertyName Vendor -NotePropertyValue $imageProfile.Vendor -Force $hostRecord | Add-Member -NotePropertyName AcceptanceLevel -NotePropertyValue $imageProfile.AcceptanceLevel -Force } } else { foreach ($hostRecord in $hostsInCluster) { $imageProfile = try { Get-VcfCheckEsxImageProfileForHost -VMHost $hostRecord.VMHost } catch { $null } $hostRecord | Add-Member -NotePropertyName ImageProfileName -NotePropertyValue $imageProfile.Name -Force $hostRecord | Add-Member -NotePropertyName Vendor -NotePropertyValue $imageProfile.Vendor -Force $hostRecord | Add-Member -NotePropertyName AcceptanceLevel -NotePropertyValue $imageProfile.AcceptanceLevel -Force } } $uniqueProfiles = @($hostsInCluster | Select-Object -Property Image, ImageProfileName, Vendor, AcceptanceLevel -Unique) $clusterId = $hostsInCluster[0].ClusterId $isImageBased = if ($clusterId -and $isImageBasedByMoRef.ContainsKey($clusterId)) { $isImageBasedByMoRef[$clusterId] } else { $true } if ($isImageBased) { $clusterSoftware = try { Get-VcfCheckClusterLcmSoftware -ClusterId $clusterId -Server $vcenterFqdn } catch { Write-LogMessage -Type WARNING -Message "vLCM software lookup for cluster `"$clusterName`" failed - AddOn/Components/FirmwareAndDriversAddOn will show `"Unavailable`" for it: $($_.Exception.Message)" $null } $lcmSummary = ConvertTo-VcfCheckLcmSoftwareSummary -Software $clusterSoftware } else { $lcmSummary = [PSCustomObject]@{ AddOn = 'N/A'; Components = @('N/A'); FirmwareAndDriversAddOn = @('N/A') } } if ($uniqueProfiles.Count -eq 1) { $commonProfile = $uniqueProfiles[0] $hostCount = $hostsInCluster.Count $rows += [PSCustomObject]@{ Cluster = $clusterName Hostname = "All $hostCount host$(if ($hostCount -ne 1) { 's' }) in the cluster have the identical image" Image = $commonProfile.Image ImageProfileName = $commonProfile.ImageProfileName Vendor = $commonProfile.Vendor AcceptanceLevel = $commonProfile.AcceptanceLevel AddOn = $lcmSummary.AddOn Components = $lcmSummary.Components FirmwareAndDriversAddOn = $lcmSummary.FirmwareAndDriversAddOn } } else { $rows += @($hostsInCluster | Sort-Object -Property Hostname | ForEach-Object { [PSCustomObject]@{ Cluster = $_.Cluster Hostname = $_.Hostname Image = $_.Image ImageProfileName = $_.ImageProfileName Vendor = $_.Vendor AcceptanceLevel = $_.AcceptanceLevel AddOn = $lcmSummary.AddOn Components = $lcmSummary.Components FirmwareAndDriversAddOn = $lcmSummary.FirmwareAndDriversAddOn } }) } } $rows = @($rows | Sort-Object -Property Cluster) [PSCustomObject]@{ VCenterFqdn = $vcenterFqdn; Status = 'Pass'; Detail = $null; Rows = $rows } } } catch { [PSCustomObject]@{ VCenterFqdn = $vcenterFqdn; Status = 'Error'; Detail = $_.Exception.Message; Rows = @() } } $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 ConvertTo-VcfCheckLcmSoftwareSummary { <# .SYNOPSIS Formats a cluster's vLCM software specification into report-ready AddOn/Components/ FirmwareAndDriversAddOn strings. .DESCRIPTION AddOn, Components, and HardwareSupport.Packages can each be null or empty when not configured on the cluster, and a HardwareSupport package entry can have blank Pkg/Version fields - all handled here so callers get a stable 'None' rather than a blank cell. Components and FirmwareAndDriversAddOn are returned as string arrays rather than a single joined string so Format-VcfCheckHtmlRowsTable can render multiple entries as a list instead of one run-on semicolon-separated cell. AddOn is returned as a single joined string - a cluster normally has at most one vendor Add-on, but Software.AddOn.Details can come back with duplicate entries for that same Add-on, so entries are de-duplicated before joining. .PARAMETER Software The EsxSettingsSoftwareInfo object from Get-VcfCheckClusterLcmSoftware, or $null if the lookup failed. .OUTPUTS [PSObject] with AddOn (String), Components (String[]), FirmwareAndDriversAddOn (String[]) - each 'Unavailable'/@('Unavailable') if -Software is $null, or 'None'/@('None') if the cluster has nothing configured for that field. #> [CmdletBinding()] [OutputType([PSObject])] Param ( [Parameter(Mandatory = $false)] [PSObject]$Software ) if ($null -eq $Software) { return [PSCustomObject]@{ AddOn = 'Unavailable'; Components = @('Unavailable'); FirmwareAndDriversAddOn = @('Unavailable') } } $addOnEntries = @($Software.AddOn.Details | ForEach-Object { $entry = "$($_.DisplayName) $($_.DisplayVersion)".Trim() if (-not [String]::IsNullOrWhiteSpace($entry)) { $entry } } | Select-Object -Unique) $addOn = if ($addOnEntries.Count -gt 0) { $addOnEntries -join '; ' } else { 'None' } $componentEntries = @($Software.Components.Values | ForEach-Object { $entry = "$($_.Details.DisplayName) $($_.Details.DisplayVersion)".Trim() if (-not [String]::IsNullOrWhiteSpace($entry)) { $entry } }) $components = if ($componentEntries.Count -gt 0) { $componentEntries } else { @('None') } $hardwareSupportEntries = @($Software.HardwareSupport.Packages.Values | ForEach-Object { $entry = "$($_.Pkg) $($_.Version)".Trim() if (-not [String]::IsNullOrWhiteSpace($entry)) { $entry } }) $firmwareAndDriversAddOn = if ($hardwareSupportEntries.Count -gt 0) { $hardwareSupportEntries } else { @('None') } return [PSCustomObject]@{ AddOn = $addOn Components = $components FirmwareAndDriversAddOn = $firmwareAndDriversAddOn } } |