Private/Checks/Esx/Test-VcfEsxLockdownStatus.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-VcfEsxLockdownStatus {

    <#
        .SYNOPSIS
        Reports lockdown mode status for every ESX host across all vCenters managed by SDDC Manager,
        flagging hosts that are missing the VCF service account SDDC Manager needs to update them
        while locked down.

        .DESCRIPTION
        Retrieves ESX host inventory for each connected vCenter domain using Get-VcfCheckVMHostInventory
        and checks the ExtensionData.Config.LockdownMode API property (HostConfigInfo.lockdownMode).

        ESX can be upgraded while Lockdown Mode is enabled as long as the host's VCF service account
        ('svc-vcf-<host_shortname>') is present in that host's Lockdown Mode Exception Users list - see
        https://knowledge.broadcom.com/external/article/435394/esxi-update-fails-with-error-upgrade-is.html.
        For each host with Lockdown Mode enabled ('lockdownNormal' or 'lockdownStrict'), queries that
        list via Get-VcfCheckHostLockdownExceptionUsers and reports the host Fail if its service
        account is absent. Hosts with Lockdown Mode disabled have no such requirement and always Pass.

        Rolls up results at the cluster level: if all hosts in a cluster share the same lockdown mode
        and service-account status, shows a single aggregated row ("X hosts in the cluster have an
        identical lockdown mode"); otherwise expands to show each host individually. Builds a
        per-vCenter table containing Cluster, Hostname, Status, ServiceAccount, and Result.

        Returns 'Fail' for a vCenter domain if any host has Lockdown Mode enabled without its VCF
        service account authorized in the Exception Users list, since SDDC Manager cannot update that
        host - the Detail field then names every affected account/host pair directly, rather than
        requiring the reader to cross-reference the Rows table to find which host needs fixing.
        Returns an 'Error' status for a domain only if an execution failure occurs (e.g., unable to
        retrieve vCenter FQDNs, connect to a vCenter, or fetch inventory).

        Iterates through all vCenters managed by SDDC Manager and delegates output generation to
        New-VcfCheckPerDomainResults to return results grouped per vCenter domain.

        .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_lockdown_status'
    $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
    }

    $outcomes = foreach ($vcenterFqdn in $vcenterFqdns) {
        $iterationStartedAt = Get-Date
        $outcome = try {
            Connect-VcfCheckVCenter -Context $Context -Fqdn $vcenterFqdn
            $hosts = @(Get-VcfCheckVMHostInventory -Server $vcenterFqdn)
            $hostIndex = 0

            $missingAccounts = [System.Collections.Generic.List[String]]::new()
            $hostsByCluster = @($hosts | ForEach-Object {
                $hostIndex++
                Write-VcfCheckSubProgress -Context $Context -Current $hostIndex -Total $hosts.Count -Label $_.Name
                $lockdownEnabled = $_.ExtensionData.Config.LockdownMode -ne 'lockdownDisabled'
                $serviceAccount = 'N/A'
                if ($lockdownEnabled) {
                    $expectedAccount = "svc-vcf-$($_.Name.Split('.')[0])"
                    $exceptionUsers = @(Get-VcfCheckHostLockdownExceptionUsers -VMHost $_ -Server $vcenterFqdn)
                    $serviceAccount = if ($exceptionUsers -icontains $expectedAccount) { 'Present' } else { 'Missing' }
                    if ($serviceAccount -eq 'Missing') {
                        $missingAccounts.Add("`"$expectedAccount`" is missing from the Exception Users list on `"$($_.Name)`"")
                    }
                }
                [PSCustomObject]@{
                    Cluster        = $_.Parent.Name
                    Hostname       = $_.Name
                    Status         = if ($lockdownEnabled) { 'Enabled' } else { 'Disabled' }
                    ServiceAccount = $serviceAccount
                    Result         = if ($serviceAccount -eq 'Missing') { 'Fail' } else { 'Pass' }
                }
            } | Group-Object -Property Cluster)

            $rows = @()
            foreach ($clusterGroup in $hostsByCluster) {
                $clusterName = $clusterGroup.Name
                $hostsInCluster = @($clusterGroup.Group)
                $uniqueStates = @($hostsInCluster | ForEach-Object { "$($_.Status)|$($_.ServiceAccount)" } | Select-Object -Unique)

                if ($uniqueStates.Count -eq 1) {
                    $hostCount = $hostsInCluster.Count
                    $rows += [PSCustomObject]@{
                        Cluster        = $clusterName
                        Hostname       = "All $hostCount host$(if ($hostCount -ne 1) { 's' }) in the cluster have an identical lockdown mode"
                        Status         = $hostsInCluster[0].Status
                        ServiceAccount = $hostsInCluster[0].ServiceAccount
                        Result         = $hostsInCluster[0].Result
                    }
                } else {
                    $rows += @($hostsInCluster | Sort-Object -Property Hostname)
                }
            }
            $rows = @($rows | Sort-Object -Property Cluster)

            $detail = $null
            if ($missingAccounts.Count -gt 0) {
                $detail = $missingAccounts -join '; '
            }

            [PSCustomObject]@{
                VCenterFqdn = $vcenterFqdn
                Status      = if (@($rows | Where-Object { $_.Result -eq 'Fail' }).Count -gt 0) { 'Fail' } else { 'Pass' }
                Detail      = $detail
                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
}