Private/Checks/Nsxt/Test-VcfNsxtBackupHistory.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-VcfNsxtBackupHistory {

    <#
        .SYNOPSIS
        Checks NSX Manager's backup history per backup type.

        .DESCRIPTION
        Uses Invoke-GetBackupHistory (VMware.Sdk.Nsx.Policy) to retrieve backup status.
        Per-type backup records (cluster/node/inventory) come from ClusterBackupStatuses,
        NodeBackupStatuses, and InventoryBackupStatuses, each a list of BackupOperationStatus
        items exposing EndTime (epoch milliseconds) and Success (bool). Each type is judged
        independently: a type Fails if its most recent backup attempt failed or has no record,
        or if its last successful backup is older than 48 hours; Warns if the last successful
        backup is between 24 and 48 hours old; otherwise Passes. The overall result Fails if any
        type Fails, Warns if any type Warns (and none Fail), and Passes only when all three types
        Pass. The top-level Detail is a plain "Pass" when every type passes, since there is
        nothing actionable in the per-type backup timestamps in that case; per-type timestamps
        are still available in Rows and are included in Detail when the result is Warning or Fail.
        Destination is read separately from Invoke-GetBackupConfig's RemoteFileServer
        (Server/Port/DirectoryPath) and reported both at the top level and as a "Backup Target"
        column on every Rows entry, since the backup target does not change per backup type.

        Remediation overrides the catalog's default "configure and run a backup" text when every
        non-passing type's Reason is Stale (a backup exists and has succeeded before, it is just
        overdue) rather than Missing or FailedAttempt - "configure and run a backup" reads oddly
        when backups are already configured and succeeding, just not recently enough; those cases
        instead point at the backup schedule/frequency.

        .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 = ''
    )

    return Invoke-VcfCheckNsxtCheck -Context $Context -CheckId 'nsxt_backup_history' -DisplayName $DisplayName -Body {
        param($Context, $NsxFqdn)
        $history = Get-VcfCheckNsxBackupHistory -Server $NsxFqdn
        $config = Get-VcfCheckNsxBackupConfig -Server $NsxFqdn
        $remote = $config.RemoteFileServer
        $destination = "$($remote.Server):$($remote.Port)$($remote.DirectoryPath)"
        $now = [DateTime]::UtcNow

        $typedBackups = @(
            @{ Name = 'Cluster'; Items = $history.ClusterBackupStatuses }
            @{ Name = 'Node'; Items = $history.NodeBackupStatuses }
            @{ Name = 'Inventory'; Items = $history.InventoryBackupStatuses }
        )

        $typeResults = @($typedBackups | ForEach-Object { Get-VcfCheckNsxBackupTypeStatus -Name $_.Name -Items $_.Items -Now $now -Destination $destination })

        $overallStatus = 'Pass'
        if ($typeResults | Where-Object { $_.Status -eq 'Fail' }) {
            $overallStatus = 'Fail'
        } elseif ($typeResults | Where-Object { $_.Status -eq 'Warning' }) {
            $overallStatus = 'Warning'
        }

        if ($overallStatus -eq 'Pass') {
            $detail = 'Pass'
        } else {
            $detailLines = @($typeResults | ForEach-Object { "$($_.Name): $($_.Status) - $($_.'Last Backup Date')" })
            $detail = $detailLines -join ' | '
        }

        $remediation = $null
        if ($overallStatus -ne 'Pass') {
            $nonPassingReasons = @($typeResults | Where-Object { $_.Status -ne 'Pass' } | Select-Object -ExpandProperty Reason)
            if (-not ($nonPassingReasons | Where-Object { $_ -ne 'Stale' })) {
                $remediation = 'The backup schedule appears to be configured and has succeeded before, but the most recent successful run is overdue - check System > Lifecycle Management > Backup & Restore for schedule/frequency issues, or trigger a manual backup now.'
            }
        }

        $rows = @($typeResults | Select-Object Name, 'Last Backup Date', 'Backup Target', Status)
        $outcome = [PSCustomObject]@{ Status = $overallStatus; Detail = $detail; Destination = $destination; Rows = $rows }
        if ($remediation) { $outcome | Add-Member -MemberType NoteProperty -Name 'Remediation' -Value $remediation }
        return $outcome
    }
}
function Get-VcfCheckNsxBackupTypeStatus {

    <#
        .SYNOPSIS
        Judges a single NSX Manager backup type against the 24h/48h freshness thresholds.

        .DESCRIPTION
        Fails when the most recent backup attempt for the type failed or no attempts exist, or
        when the last successful backup is older than 48 hours. Warns when the last successful
        backup is between 24 and 48 hours old. Otherwise Passes.

        The returned Reason ('Missing', 'FailedAttempt', 'Stale', or 'Pass') lets the caller pick
        a remediation message without re-deriving it from the Status/"Last Backup Date" text - a
        non-passing type with no prior successful backup needs different guidance than one that
        has succeeded before but is overdue.

        .PARAMETER Name
        The backup type name (Cluster, Node, or Inventory) used for display purposes.

        .PARAMETER Items
        The list of BackupOperationStatus items for this backup type.

        .PARAMETER Now
        The current UTC time used to compute backup age.

        .PARAMETER Destination
        The backup target (server, port, and directory) to display on the row.

        .OUTPUTS
        [PSObject] with Name, "Last Backup Date", "Backup Target", Status, and Reason properties.
    #>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Name,
        [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [AllowNull()] [Object[]]$Items,
        [Parameter(Mandatory = $true)] [DateTime]$Now,
        [Parameter(Mandatory = $true)] [String]$Destination
    )

    $items = @($Items)
    if ($items.Count -eq 0) {
        return [PSCustomObject]@{ Name = $Name; 'Last Backup Date' = 'No backup on record.'; 'Backup Target' = $Destination; Status = 'Fail'; Reason = 'Missing' }
    }

    $lastAttempt = @($items | Sort-Object -Property EndTime -Descending | Select-Object -First 1)[0]
    if (-not $lastAttempt.Success) {
        return [PSCustomObject]@{ Name = $Name; 'Last Backup Date' = 'Most recent backup attempt failed.'; 'Backup Target' = $Destination; Status = 'Fail'; Reason = 'FailedAttempt' }
    }

    $lastSuccessTime = [DateTimeOffset]::FromUnixTimeMilliseconds($lastAttempt.EndTime).UtcDateTime
    $ageHours = ($Now - $lastSuccessTime).TotalHours

    if ($ageHours -gt 48) {
        return [PSCustomObject]@{ Name = $Name; 'Last Backup Date' = "$($lastSuccessTime.ToString('o')), more than 48 hours ago."; 'Backup Target' = $Destination; Status = 'Fail'; Reason = 'Stale' }
    }

    if ($ageHours -gt 24) {
        return [PSCustomObject]@{ Name = $Name; 'Last Backup Date' = "$($lastSuccessTime.ToString('o')), more than 24 hours ago."; 'Backup Target' = $Destination; Status = 'Warning'; Reason = 'Stale' }
    }

    return [PSCustomObject]@{ Name = $Name; 'Last Backup Date' = $lastSuccessTime.ToString('o'); 'Backup Target' = $Destination; Status = 'Pass'; Reason = 'Pass' }
}