Private/Checks/VCenter/Test-VcfVcenterCheckDiskSpaceInodeAndHprof.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 Get-VcfCheckDiskSpaceInodeAndHprofResult {

    <#
        .SYNOPSIS
        Runs and evaluates the disk space/inode/hprof guest command for a single vCenter appliance.

        .DESCRIPTION
        Shared body for Test-VcfVcenterCheckDiskSpaceInodeAndHprof, called once per vCenter FQDN
        with -Server set to the management vCenter (SDDC-Manager-managed appliances) or the
        appliance's own FQDN (self-managed, e.g. an Aria-only vCenter). Runs `df -h`/`df -i`/a
        `.hprof` scan via
        Invoke-VcfApplianceCommand against -Server, using -Credential, and flags any filesystem at
        or above 80% space/inode utilization or any leftover heap dump file.

        .PARAMETER VmName
        Short VM name of the vCenter appliance (its FQDN's hostname label).

        .PARAMETER Server
        FQDN of the vCenter to run the guest operation through - the management vCenter for an
        SDDC-Manager-managed appliance, or the appliance's own FQDN when it is a self-managed
        Aria-only vCenter.

        .PARAMETER Credential
        GuestOS root credential for the target appliance.

        .OUTPUTS
        [PSCustomObject] with Status ('Pass'/'Warning'/'Error'/'Skipped'), Detail, Rows, and
        (only on Skipped) SkipReasonTag properties.
    #>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$VmName,
        [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Server,
        [Parameter(Mandatory = $true)] [PSCredential]$Credential
    )

    $commandResult = Invoke-VcfApplianceCommand -VmName $VmName -Server $Server -Credential $Credential `
        -ScriptText 'df -h --output=target,pcent | tail -n +2; echo "---INODES---"; df -i --output=target,ipcent | tail -n +2; echo "---HPROF---"; find / -xdev -name "*.hprof" 2>/dev/null'

    if (-not $commandResult.Success) {
        if ($commandResult.ErrorCategory -eq 'ToolsNotRunning') {
            $detail = "This check cannot run without VMware Tools running on the target appliance - skipped for now. $($commandResult.ErrorMessage)"
            return [PSCustomObject]@{ Status = 'Skipped'; Detail = $detail; SkipReasonTag = 'VMware Tools not running'; Rows = @() }
        }
        return [PSCustomObject]@{ Status = 'Error'; Detail = $commandResult.ErrorMessage; Rows = @() }
    }

    $sections = $commandResult.ScriptOutput -split '---INODES---|---HPROF---'
    $filesystemData = @{}
    $flagged = [System.Collections.Generic.List[String]]::new()

    foreach ($line in ($sections[0] -split "`n")) {
        $line = $line.Trim()
        if (-not $line) { continue }
        $parts = $line -split '\s+'
        if ($parts.Count -lt 2) { continue }
        $mountPoint = $parts[-1]
        $percentStr = $parts[-2]
        $percent = 0
        if ([Int32]::TryParse(($percentStr -replace '%', ''), [ref]$percent)) {
            if (-not $filesystemData[$mountPoint]) { $filesystemData[$mountPoint] = @{} }
            $filesystemData[$mountPoint]['SpacePercent'] = $percent
            if ($percent -ge 80) {
                $flagged.Add("$mountPoint (space): $percent%")
            }
        }
    }

    foreach ($line in ($sections[1] -split "`n")) {
        $line = $line.Trim()
        if (-not $line) { continue }
        $parts = $line -split '\s+'
        if ($parts.Count -lt 2) { continue }
        $mountPoint = $parts[0]
        $percent = 0
        if ([Int32]::TryParse(($parts[1] -replace '%', ''), [ref]$percent)) {
            if (-not $filesystemData[$mountPoint]) { $filesystemData[$mountPoint] = @{} }
            $filesystemData[$mountPoint]['InodePercent'] = $percent
            if ($percent -ge 80) {
                $flagged.Add("$mountPoint (inode): $percent%")
            }
        }
    }

    $hprofFiles = @($sections[2] -split "`n" | Where-Object { -not [String]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })

    $rows = @($filesystemData.Keys | ForEach-Object {
        $target = $_
        $data = $filesystemData[$target]
        $spacePercent = if ($data.ContainsKey('SpacePercent')) { $data['SpacePercent'] } else { 0 }
        $inodePercent = if ($data.ContainsKey('InodePercent')) { $data['InodePercent'] } else { 0 }
        $status = if ($spacePercent -ge 80) { 'Warning' } else { 'Pass' }
        [PSCustomObject]@{ MountedOn = $target; SpaceUsedPercent = $spacePercent; InodeUsedPercent = $inodePercent; Status = $status }
    })

    $detailParts = [System.Collections.Generic.List[String]]::new()
    if ($flagged.Count -gt 0) {
        $detailParts.Add("Filesystem(s) at or above 80% utilization: $($flagged -join '; ')")
    }
    if ($hprofFiles.Count -gt 0) {
        $detailParts.Add("Found $($hprofFiles.Count) heap dump file(s): $($hprofFiles -join '; ')")
    }

    if ($detailParts.Count -gt 0) {
        return [PSCustomObject]@{ Status = 'Warning'; Detail = ($detailParts -join ' '); Rows = $rows }
    }

    return [PSCustomObject]@{ Status = 'Pass'; Detail = 'Every filesystem is below 80% disk space and inode utilization, and no heap dump files were found.'; Rows = $rows }
}
function Test-VcfVcenterCheckDiskSpaceInodeAndHprof {

    <#
        .SYNOPSIS
        Checks disk space and inode utilization on the vCenter appliance's filesystems, and scans
        for leftover Java heap-dump (.hprof) files.

        .DESCRIPTION
        Executes guest commands via Invoke-VcfApplianceCommand on every vCenter appliance VM known
        to VcfCheck - both vCenters SDDC Manager manages and vCenters SDDC Manager does not manage
        but that an Aria component references (Context.AriaOnlyVCenterFqdns) - to inspect
        filesystem capacity, inode utilization, and leftover Java heap dump (.hprof) files in a
        single appliance execution per vCenter.

        Disk Space and Inode Evaluation:
        - Executes `df -h` and `df -i` to measure disk space and inode usage.
        - Any filesystem reporting disk space or inode utilization at or above 80% returns a Warning status.

        Heap Dump Scanning:
        - Executes `find / -xdev -name '*.hprof' 2>/dev/null` to identify leftover heap dumps from prior
          service crashes.
        - Any detected .hprof files raise a Warning status and list the file paths in the result detail.

        Target Addressing:
        - A vCenter managed by SDDC Manager has its appliance VM on management domain compute, so
          the guest command is run through the management vCenter (Get-VcfCheckManagementVCenterFqdn).
        - A vCenter in Context.AriaOnlyVCenterFqdns is self-managed (its appliance VM runs on its
          own compute), so the guest command is run through the appliance's own FQDN instead, and
          its root credential is resolved via Get-VcfCheckComponentCredential's Aria-aware branch
          (Get-VcfCheckAriaVCenterRootCredential) rather than the SDDC Manager credentials API.
        - Evaluates each vCenter and delegates per-domain result aggregation to
          Invoke-VcfCheckPerVCenterCheck.

        .PARAMETER Context
        The VcfCheck.Context object. Must already be connected to SDDC Manager.

        .OUTPUTS
        [PSObject] a VcfCheck.Result.
    #>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Context,
        [Parameter(Mandatory = $false)] [String]$DisplayName = ''
    )

    $startedAt = Get-Date
    $checkId = 'vcenter_disk_space_inode_and_hprof_check'
    $catalogEntry = (Get-VcfCheckCatalog)[$checkId]
    $displayName = if ([String]::IsNullOrEmpty($DisplayName)) { $catalogEntry.displayName } else { $DisplayName }

    try {
        $managementVCenterFqdn = Get-VcfCheckManagementVCenterFqdn -Context $Context
    } catch {
        return New-VcfCheckResult -CheckId $checkId -Area vCenter -Status Error `
            -Exception $_.Exception.Message -ValidationCriteria $catalogEntry.validationCriteria -Remediation $catalogEntry.remediation -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $displayName
    }

    return Invoke-VcfCheckPerVCenterCheck -Context $Context -CheckId $checkId -Area vCenter -DisplayName $DisplayName -Body {
        param($Context, $VCenterFqdn)
        $isAriaOnly = $Context.AriaOnlyVCenterFqdns.ContainsKey($VCenterFqdn)
        $server = if ($isAriaOnly) { $VCenterFqdn } else { $managementVCenterFqdn }
        Connect-VcfCheckVCenter -Context $Context -Fqdn $server
        $rootCredential = Get-VcfCheckComponentCredential -Context $Context -ResourceType VCENTER -AccountType USER -Fqdn $VCenterFqdn -Username 'root'
        $vmName = ($VCenterFqdn -split '\.')[0]
        return Get-VcfCheckDiskSpaceInodeAndHprofResult -VmName $vmName -Server $server -Credential $rootCredential
    }
}