Private/VrslcmHelpers.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.
#
# =============================================================================
#region VrslcmHelpers
#
# Aria Suite Lifecycle Manager (VRSLCM) has no PowerCLI/OpenAPI SDK coverage. VRSLCM is a
# distinct product with its own /lcm/... REST API, not reachable through VMware.Sdk.Vcf.Ops
# (Aria Operations) or VMware.Sdk.Vr (vSphere Replication). REST helper calls are required.
#
# VRSLCM has no per-domain FQDN lookup - Invoke-VcfGetCredentials -ResourceType VRSLCM is the
# only source for both its FQDN and credential. Credential filtering on CredentialType -eq 'API'
# is done client-side because Invoke-VcfGetCredentials lacks a -CredentialType parameter
# (only -AccountType is available).

function Get-VcfCheckVrslcmConnection {
    <#
        .SYNOPSIS
        Resolves and caches Aria Suite Lifecycle Manager's FQDN and API credential.

        .DESCRIPTION
        Calls Invoke-VcfGetCredentials -ResourceType VRSLCM once per run and caches the result on
        $Context. Returns $null (not an exception) when no matching credential entry comes back -
        this is the expected, normal shape of "VRSLCM is not deployed in this environment", which
        every VRSLCM-area check must treat as Skipped rather than Error.

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

        .OUTPUTS
        [PSCustomObject] with Fqdn/Credential properties, or $null if VRSLCM is not deployed.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Context
    )

    if ($Context.VrslcmConnection) {
        return $Context.VrslcmConnection
    }

    Write-LogMessage -Type DEBUG -Message 'Resolving Aria Suite Lifecycle Manager (VRSLCM) FQDN and credential from SDDC Manager.'
    try {
        $response = Invoke-VcfGetCredentials -ResourceType VRSLCM -ErrorAction Stop
    } catch {
        throw [System.InvalidOperationException]::new("Failed to query SDDC Manager for VRSLCM credentials: $($_.Exception.Message)")
    }

    $match = @($response.Elements) | Where-Object { $_.CredentialType -eq 'API' } | Select-Object -First 1
    if (-not $match) {
        Write-LogMessage -Type DEBUG -Message 'No VRSLCM credential entry returned by SDDC Manager - Aria Suite Lifecycle Manager is not part of this environment.'
        return $null
    }

    $secure = ConvertTo-SecureStringForCredential -PlainText $match.Password
    $credential = [PSCredential]::new($match.Username, $secure)
    $connection = [PSCustomObject]@{
        PSTypeName       = 'VcfCheck.VrslcmConnection'
        Fqdn             = $match.Resource.ResourceName
        Credential       = $credential
        AllowInsecureTls = [Bool]$Context.AllowInsecureTls
    }
    Remove-Variable -Name match, response -ErrorAction SilentlyContinue

    $Context.VrslcmConnection = $connection
    return $connection
}
function Get-VcfCheckVrslcmRootCredential {
    <#
        .SYNOPSIS
        Resolves the Aria Suite Lifecycle Manager appliance's root/OS credential.

        .DESCRIPTION
        VRSLCM's locker password-decrypt API ('/lcm/locker/api/passwords/view/{vmid}') requires the
        appliance's own root password as its "rootPassword" request body field - not the admin@local
        API credential used by Invoke-VcfCheckVrslcmApi. Filters the same
        Invoke-VcfGetCredentials -ResourceType VRSLCM response on CredentialType -eq 'SSH' instead of
        'API' (see Get-VcfCheckVrslcmConnection). Returns $null (not an exception) when no matching
        entry comes back, matching the "VRSLCM not deployed" Skipped convention used elsewhere in this
        file.

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

        .OUTPUTS
        [PSCredential], or $null if VRSLCM is not deployed or has no SSH credential entry.
    #>

    [CmdletBinding()]
    [OutputType([PSCredential])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Context
    )

    if ($Context.VrslcmRootCredential) {
        return $Context.VrslcmRootCredential
    }

    Write-LogMessage -Type DEBUG -Message 'Resolving Aria Suite Lifecycle Manager (VRSLCM) root credential from SDDC Manager.'
    try {
        $response = Invoke-VcfGetCredentials -ResourceType VRSLCM -ErrorAction Stop
    } catch {
        throw [System.InvalidOperationException]::new("Failed to query SDDC Manager for VRSLCM credentials: $($_.Exception.Message)")
    }

    $match = @($response.Elements) | Where-Object { $_.CredentialType -eq 'SSH' } | Select-Object -First 1
    if (-not $match) {
        Write-LogMessage -Type DEBUG -Message 'No VRSLCM root (SSH) credential entry returned by SDDC Manager.'
        return $null
    }

    $secure = ConvertTo-SecureStringForCredential -PlainText $match.Password
    $credential = [PSCredential]::new($match.Username, $secure)
    Remove-Variable -Name match, response -ErrorAction SilentlyContinue

    $Context.VrslcmRootCredential = $credential
    return $credential
}
function Invoke-VcfCheckVrslcmApi {
    <#
        .SYNOPSIS
        Calls Aria Suite Lifecycle Manager's REST API with HTTP Basic Auth.

        .DESCRIPTION
        Thin Invoke-RestMethod wrapper - no PowerCLI/OpenAPI SDK covers VRSLCM (see file header).
        -SkipCertificateCheck is only passed when Connection.AllowInsecureTls is $true (the run's
        resolved AllowInsecureTls value, derived from PowerCLI's InvalidCertificateAction setting -
        see Invoke-VcfCheck in Orchestrator.ps1 and Get-VcfCheckVrslcmConnection) rather than being
        unconditional; an untrusted certificate
        encountered while that is $false is translated into a clear, actionable error by
        ConvertTo-VcfCheckFriendlyVrslcmError instead of being silently accepted.

        Retries up to 3 times with a 10-second delay on transient network failures (timeouts,
        connection resets) to prevent transient vRSLCM API blips from causing terminal failures.

        .PARAMETER Connection
        The object returned by Get-VcfCheckVrslcmConnection.

        .PARAMETER Method
        HTTP method. Defaults to GET.

        .PARAMETER Path
        The VRSLCM API path, e.g. '/lcm/lcops/api/v2/environments'.

        .PARAMETER Body
        Optional request body, sent as JSON. Only used for non-GET methods.

        .OUTPUTS
        The parsed JSON response.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Connection,
        [Parameter(Mandatory = $false)] [ValidateSet('GET', 'POST')] [String]$Method = 'GET',
        [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Path,
        [Parameter(Mandatory = $false)] [AllowNull()] [PSObject]$Body = $null
    )

    $uri = "https://$($Connection.Fqdn)$Path"
    $parameters = @{
        Uri                  = $uri
        Method               = $Method
        Credential           = $Connection.Credential
        Authentication       = 'Basic'
        ContentType          = 'application/json'
        SkipCertificateCheck = $Connection.AllowInsecureTls
        ErrorAction          = 'Stop'
    }
    if ($Body) {
        $bodyJson = $null
        foreach ($depth in @(5, 3, 2)) {
            try {
                $bodyJson = $Body | ConvertTo-Json -Depth $depth -ErrorAction Stop
                break
            }
            catch {
                if ($depth -eq 2) {
                    throw $_
                }
            }
        }
        $parameters['Body'] = $bodyJson
    }

    $maxAttempts = 3
    $retryDelaySeconds = 10
    for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
        try {
            return Invoke-RestMethod @parameters
        } catch {
            $isTransient = $_.Exception.Message -match '(?i)timed out|timeout|actively refused|connection refused|reset by peer|handshake'
            if (-not $isTransient -or $attempt -eq $maxAttempts) {
                throw
            }
            Write-LogMessage -Type DEBUG -Message "Transient error calling Aria Suite Lifecycle Manager API '$Path' (attempt $attempt of $maxAttempts): $($_.Exception.Message). Retrying in $retryDelaySeconds seconds."
            Start-Sleep -Seconds $retryDelaySeconds
        }
    }
}
function ConvertTo-VcfCheckFriendlyVrslcmError {
    <#
        .SYNOPSIS
        Translates a raw VRSLCM API connection exception into a user-facing message.

        .DESCRIPTION
        Invoke-VcfCheckVrslcmApi surfaces raw .NET/PowerShell exception text (e.g. "The SSL
        connection could not be established, see inner exception.") verbatim - confusing to a
        user who has no reason to know VRSLCM's checks talk to it over HTTPS internally. Checks
        Get-VcfCheckTlsTrustErrorMessage first, so a certificate-trust failure (only reachable
        when AllowInsecureTls is $false - see Invoke-VcfCheckVrslcmApi) gets its specific,
        actionable message rather than the generic SSL-handshake wording below. Recognizes the
        remaining common network-failure shapes (connection refused, DNS/name resolution, timeout)
        and rewords them; any other exception message is passed through unchanged since it likely
        already describes an API-level (not network) problem more usefully than a generic
        rewording would.

        .PARAMETER Fqdn
        The VRSLCM FQDN the failed request was made to.

        .PARAMETER ErrorMessage
        The raw exception message to translate.

        .OUTPUTS
        [String] a user-facing error message.
    #>

    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Fqdn,
        [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$ErrorMessage
    )
    $tlsMessage = Get-VcfCheckTlsTrustErrorMessage -ComponentName 'Aria Suite Lifecycle Manager' -Fqdn $Fqdn -ErrorMessage $ErrorMessage
    if ($tlsMessage) {
        return $tlsMessage
    }
    if ($ErrorMessage -match '(?i)SSL connection could not be established|handshake') {
        return "Could not establish an SSL connection to Aria Suite Lifecycle Manager `"$Fqdn`". Verify the appliance is powered on, reachable on port 443, and that VPN/firewall rules allow the connection."
    }
    if ($ErrorMessage -match '(?i)actively refused|connection refused') {
        return "Aria Suite Lifecycle Manager `"$Fqdn`" refused the connection on port 443. Verify the appliance is powered on and its service is running."
    }
    if ($ErrorMessage -match '(?i)No such host is known|could not be resolved|name or service not known') {
        return "Could not resolve Aria Suite Lifecycle Manager's FQDN `"$Fqdn`". Verify DNS resolution."
    }
    if ($ErrorMessage -match '(?i)timed out|timeout') {
        return "Connection to Aria Suite Lifecycle Manager `"$Fqdn`" timed out. Verify network connectivity, VPN, and firewall rules."
    }
    return $ErrorMessage
}

#endregion VrslcmHelpers