clustering.psm1

# Helper
function Resolve-MemberAppliance
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$true,Position=0)]
        [string]$Member,
        [Parameter(Mandatory=$false)]
        [switch]$WithHealth
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    $local:GetHealth = ""
    if ($WithHealth)
    {
        $local:GetHealth = ",Health"
    }
    try
    {
        Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core GET "Cluster/Members/$Member" `
            -Parameters @{ fields = "Id,IsLeader,Name,Ipv4Address,Ipv6Address,SslCertificateThumbprint,EnrolledSince$($local:GetHealth)" } `
            -RetryUrl "ClusterMembers/$Member"
    }
    catch
    {
        $local:Members = (Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core GET "Cluster/Members" `
                              -Parameters @{ filter = "(Id ieq '$Member') or (Name ieq '$Member') or (Ipv4Address eq '$Member') or (Ipv6Address ieq '$Member')";
                                             fields = "Id,IsLeader,Name,Ipv4Address,Ipv6Address,SslCertificateThumbprint,EnrolledSince$($local:GetHealth)" } `
                              -RetryUrl "ClusterMembers")
        if (-not $local:Members)
        {
            throw "Unable to find cluster member matching '$Member'"
        }
        $local:Members[0]
    }
}
function Resolve-MemberApplianceId
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$true,Position=0)]
        [string]$Member
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    (Resolve-MemberAppliance -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure $Member).Id
}
function Get-ClusterConnectivityReachabilityError
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0)]
        [object]$HealthObjectSpecific
    )

    $local:Error = ""
    $HealthObjectSpecific.NodeConnectivity | ForEach-Object {
        if ($_.IsReachable -eq $false)
        {
            if (-not [string]::IsNullOrEmpty($local:Error))
            {
                $local:Error += ", "
            }
            $local:Error += "$($_.ApplianceId) is unreachable"
        }
    }
    $local:Error
}
function Get-ClusterHealthError
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0)]
        [string]$Id,
        [Parameter(Mandatory=$true,Position=1)]
        [string]$Name,
        [Parameter(Mandatory=$true,Position=2)]
        [string]$HealthType,
        [Parameter(Mandatory=$true,Position=3)]
        [object]$State,
        [Parameter(Mandatory=$true,Position=4)]
        [object]$HealthObjectSpecific
    )

    if ($State -eq "Quarantine")
    {
        "$Id ($Name) $HealthType Error: $Id is in Quarantine"
    }
    elseif ($State -eq "Offline")
    {
        "$Id ($Name) $HealthType Error: $Id is in Offline"
    }
    else
    {
        if ($HealthType -eq "Cluster Connectivity")
        {
            "$Id ($Name) $HealthType Error: $(Get-ClusterConnectivityReachabilityError $HealthObjectSpecific)"
        }
        else
        {
            "$Id ($Name) $HealthType Error: $($HealthObjectSpecific.Error)"
        }
    }
}
function Get-Reachable
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0)]
        $Member,
        [Parameter(Mandatory=$true,Position=1)]
        [string]$Id
    )

    if ($Member.Id -eq $Id)
    {
        "$([Char]8730)"
    }
    elseif (-not ($Member.Health.ClusterConnectivity.NodeConnectivity))
    {
        "X"
    }
    elseif (-not ($Member.Health.ClusterConnectivity.NodeConnectivity | Where-Object { $_.ApplianceId -eq $Id }))
    {
        "X"
    }
    elseif (($Member.Health.ClusterConnectivity.NodeConnectivity | Where-Object { $_.ApplianceId -eq $Id }).IsReachable)
    {
        "$([Char]8730)"
    }
    else
    {
        "X"
    }
}
function Get-ReachableMatrix
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0)]
        $Members
    )

    $Members | ForEach-Object {
        $local:Reachable = New-Object -TypeName PSObject -Property @{
            Name = $_.Name
        }
        $local:Id = $_.Id
        $Members | ForEach-Object {
            $local:Reachable | Add-Member -MemberType NoteProperty -Name $_.Name -Value (Get-Reachable $_ $local:Id)
        }
        $local:Reachable
    }
}

<#
.SYNOPSIS
Get cluster members from Safeguard via the Web API.

.DESCRIPTION
Retrieve the list of Safeguard appliances in this cluster from the Web API.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.PARAMETER Member
A string containing an ID, name, or network address for the member appliance.

.PARAMETER WithHealth
Include the health information in the results.

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Get-SafeguardClusterMember -AccessToken $token -Appliance 10.5.32.54

.EXAMPLE
Get-SafeguardClusterMember

.EXAMPLE
Get-SafeguardClusterMember 10.5.33.144

.EXAMPLE
Get-SafeguardClusterMember SG-00155D26E38A
#>

function Get-SafeguardClusterMember
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$false,Position=0)]
        [string]$Member,
        [Parameter(Mandatory=$false)]
        [switch]$WithHealth
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    if ($Member)
    {
        Write-Verbose "Getting specific appliance '$AccessToken' '$Appliance' '$Insecure' '$Member'"
        Resolve-MemberAppliance -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure $Member -WithHealth:$WithHealth
    }
    else
    {
        if ($WithHealth)
        {
            $local:GetHealth = ",Health"
        }
        Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core GET "Cluster/Members" `
            -RetryUrl "ClusterMembers" -Parameters @{ fields = "Id,IsLeader,Name,Ipv4Address,Ipv6Address,SslCertificateThumbprint,EnrolledSince$($local:GetHealth)" }
    }
}

<#
.SYNOPSIS
Get health of cluster members from Safeguard via the Web API.

.DESCRIPTION
Retrieve the information based on most recent health check for all Safeguard appliances
in this cluster via the Web API.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.PARAMETER Member
A string containing an ID, name, or network address for the member appliance.

.PARAMETER Category
A string containing the type of health information to expand in the results.

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Get-SafeguardClusterHealth -AccessToken $token -Appliance 10.5.32.54

.EXAMPLE
Get-SafeguardClusterHealth 10.5.33.144
#>

function Get-SafeguardClusterHealth
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$false,Position=0)]
        [string]$Member,
        [ValidateSet("AuditLog", "ClusterCommunication", "ClusterConnectivity", "AccessWorkflow", "PolicyData", `
                     "ResourceUsage", "SessionModule", "NodeConnectivity", IgnoreCase=$true)]
        [Parameter(Mandatory=$false,Position=1)]
        [string]$Category
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    if ($Member)
    {
        $local:Health = (Resolve-MemberAppliance -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure $Member -WithHealth).Health
    }
    else
    {
        $local:Health = (Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core GET "Cluster/Members" `
            -RetryUrl "ClusterMembers").Health
    }
    if ($Category)
    {
        if ($Category -ieq "NodeConnectivity")
        {
            $local:Health | Select-Object -ExpandProperty "ClusterConnectivity" | Select-Object -ExpandProperty $Category | Format-List
        }
        else
        {
            $local:Health | Select-Object -ExpandProperty $Category | Format-List
        }
    }
    else
    {
        $local:Health | Format-List
    }
}

<#
.SYNOPSIS
Add a new replica to the cluster via the Safeguard Web API.

.DESCRIPTION
Enroll a new replica into the cluster. This cmdlet kicks off the enrollment process and
waits for it to complete unless the -NoWait flag is specified. In order for enrollment
to work you have to be able to authenticate to the new replica as well.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.PARAMETER ReplicaNetworkAddress
A string containing the network address of the replica to add to the cluster.

.PARAMETER ReplicaAccessToken
A string containing the access token for the replica.

.PARAMETER ReplicaGui
Specify this flag to display the GUI login experience to authenticate to the replica (required for 2FA).

.PARAMETER NoWait
Specify this flag to continue immediately without waiting for the enrollment to complete.

.PARAMETER Timeout
A timeout value in seconds for adding cluster member (default: 1800s or 30m)

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Add-SafeguardClusterMember -AccessToken $token -Appliance 10.5.32.54 -ReplicaNetworkAddress 10.5.33.144

.EXAMPLE
Add-SafeguardClusterMember 10.5.33.144 -ReplicaAccessToken $tok -NoWait

.EXAMPLE
Add-SafeguardClusterMember 10.5.33.144 -ReplicaGui
#>

function Add-SafeguardClusterMember
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$true,Position=0)]
        [string]$ReplicaNetworkAddress,
        [Parameter(Mandatory=$false)]
        [object]$ReplicaAccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$ReplicaGui,
        [Parameter(Mandatory=$false)]
        [switch]$NoWait,
        [Parameter(Mandatory=$false)]
        [int]$Timeout = 1800
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    Import-Module -Name "$PSScriptRoot\sg-utilities.psm1" -Scope Local

    if (-not $PSBoundParameters.ContainsKey("ReplicaAccessToken"))
    {
        Write-Host "Authenticating to new replica appliance '$ReplicaNetworkAddress'..."
        if (-not ($PSBoundParameters.ContainsKey("Insecure")) -and $SafeguardSession)
        {
            # This only covers the case where Invoke-SafeguardMethod is called directly.
            # All script callers in the module will specify the flag, e.g. -Insecure:$Insecure
            # which will not hit this code.
            $Insecure = $SafeguardSession["Insecure"]
        }
        $ReplicaAccessToken = (Connect-Safeguard -Insecure:$Insecure $ReplicaNetworkAddress -Gui:$ReplicaGui -NoSessionVariable)
    }

    if (-not $ReplicaAccessToken)
    {
        throw "Failed to authenticate to replica '$ReplicaNetworkAddress'"
    }

    if (-not $Appliance -and $SafeguardSession)
    {
        $Appliance = $SafeguardSession["Appliance"]
    }

    Write-Host "Joining '$ReplicaNetworkAddress' to cluster (primary: '$Appliance')..."
    Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core POST "Cluster/Members" `
            -Body @{ Hostname = $ReplicaNetworkAddress; AuthenticationToken = $ReplicaAccessToken } -RetryUrl "ClusterMembers"

    if (-not $NoWait)
    {
        Wait-ForClusterOperation -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure -Timeout $Timeout
    }
    else
    {
        Write-Host "Not waiting for completion--use Get-SafeguardClusterMember and Get-SafeguardClusterOperationStatus to see status"
    }
}

<#
.SYNOPSIS
Remove a replica from the cluster via the Safeguard Web API.

.DESCRIPTION
Remove a replica from the cluster. This cmdlet kicks off the removal process but
does not wait for it to complete. Unjoining takes very little time for the remaining
cluster members but it takes a long time for the unjoined replica to reconfigure itself
outside the cluster. Use Get-SafeguardStatus to see when the unjoined replica is ready.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.PARAMETER Member
A string containing an ID, name, or network address for the member appliance.

.PARAMETER Force
Specify this flag to force remove the an appliance without quorum.

.PARAMETER Wait
Specify this flag to wait for the cluster remove operation to complete.

.PARAMETER Timeout
Timeout in seconds for the Wait parameter (default: 1200 seconds or 20 minutes)

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Remove-SafeguardClusterMember SG-00155D26E38D

.EXAMPLE
Remove-SafeguardClusterMember 10.5.33.144 -Force
#>

function Remove-SafeguardClusterMember
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$true,Position=0)]
        [string]$Member,
        [Parameter(Mandatory=$false)]
        [switch]$Force,
        [Parameter(Mandatory=$false)]
        [switch]$Wait = $false,
        [Parameter(Mandatory=$false)]
        [int]$Timeout = 1200
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    $local:MemberId = (Resolve-MemberApplianceId -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure $Member)
    if (-not $Force)
    {
        Import-Module -Name "$PSScriptRoot\sg-utilities.psm1" -Scope Local
        Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core DELETE "Cluster/Members/$MemberId" `
            -RetryUrl "ClusterMembers/$MemberId"
        Wait-ForClusterOperation -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure -Timeout $Timeout
    }
    else
    {
        Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core POST "Cluster/Members/Reset" `
            -JsonBody "{`"Members`": [{`"Id`": `"$($local:MemberId)`",`"IsLeader`": true}],`"PrimaryId`": `"$($local:MemberId)`"}" `
            -RetryUrl "ClusterMembers/Reset"
    }

    Write-Host "Not waiting for completion--use Get-SafeguardStatus and Get-SafeguardClusterOperationStatus to see status"
}

<#
.SYNOPSIS
Get cluster primary from Safeguard via the Web API.

.DESCRIPTION
Retrieve current primary appliance in this cluster from the Web API.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.PARAMETER Member
A string containing an ID, name, or network address for the member appliance.

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Get-SafeguardClusterPrimary -AccessToken $SafeguardSession.AccessToken -Appliance 10.5.32.54

.EXAMPLE
Get-SafeguardClusterPrimary
#>

function Get-SafeguardClusterPrimary
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core GET "Cluster/Members" `
            -Parameters @{fields = "Id,IsLeader,Name,Ipv4Address,Ipv6Address,SslCertificateThumbprint,EnrolledSince"
                          filter = "IsLeader eq true"} -RetryUrl "ClusterMembers"
}

<#
.SYNOPSIS
Set appliance as primary for the cluster via the Safeguard Web API.

.DESCRIPTION
This cmdlet can be used to perform replica failover. This cmdlet starts the process
of reconfiguring the specified appliance as the primary.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.PARAMETER Member
A string containing an ID, name, or network address for the member appliance.

.PARAMETER NoWait
Specify this flag to continue immediately without waiting for the failover to complete.

.PARAMETER Timeout
A timeout value in seconds for setting cluster primary (default: 600s or 10m)

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Set-SafeguardClusterPrimary -AccessToken $token -Appliance 10.5.33.144 10.5.33.144

.EXAMPLE
Set-SafeguardClusterPrimary SG-00155D26E38D

.EXAMPLE
Set-SafeguardClusterPrimary 10.5.33.144 -Force
#>

function Set-SafeguardClusterPrimary
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$true,Position=0)]
        [string]$Member,
        [Parameter(Mandatory=$false)]
        [switch]$NoWait,
        [Parameter(Mandatory=$false)]
        [int]$Timeout = 600
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    Import-Module -Name "$PSScriptRoot\sg-utilities.psm1" -Scope Local

    $MemberId = (Resolve-MemberApplianceId -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure $Member)
    Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core POST "Cluster/Members/$MemberId/Promote" `
        -RetryUrl "ClusterMembers/$MemberId/Promote"

    if (-not $NoWait)
    {
        Wait-ForClusterOperation -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure -Timeout $Timeout
    }
    else
    {
        Write-Host "Not waiting for completion--use Get-SafeguardClusterMember and Get-SafeguardClusterOperationStatus to see status"
    }
}

<#
.SYNOPSIS
Enable current primary appliance in the cluster via the Safeguard Web API.

.DESCRIPTION
This cmdlet can be used to activate a primary that is in StandaloneReadOnly mode.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.PARAMETER NoWait
Specify this flag to continue immediately without waiting for the restore to complete.

.PARAMETER Timeout
A timeout value in seconds for enable (default: 600s or 10m)

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Enable-SafeguardClusterPrimary -AccessToken $token -Appliance 10.5.33.144

.EXAMPLE
Enable-SafeguardClusterPrimary
#>

function Enable-SafeguardClusterPrimary
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$false)]
        [switch]$NoWait,
        [Parameter(Mandatory=$false)]
        [int]$Timeout = 600
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    Import-Module -Name "$PSScriptRoot\sg-utilities.psm1" -Scope Local

    Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core POST "Cluster/Members/ActivatePrimary" `
        -RetryUrl "ClusterMembers/ActivatePrimary"

    if (-not $NoWait)
    {
        Wait-ForSafeguardOnlineStatus -Appliance $Appliance -Insecure:$Insecure -Timeout $Timeout
    }
}

<#
.SYNOPSIS
Get the status of a currently running Safeguard cluster operation via the Safeguard Web API.

.DESCRIPTION
This cmdlet can be used to determine if any cluster operation has completed. When return value
reports the current operation as None, then the operation is complete.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Get-SafeguardClusterOperationStatus -AccessToken $token -Appliance 10.5.33.144

.EXAMPLE
Get-SafeguardClusterOperationStatus
#>

function Get-SafeguardClusterOperationStatus
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure
    )

    Import-Module -Name "$PSScriptRoot\sg-utilities.psm1" -Scope Local

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core GET "Cluster/Status" `
        -RetryUrl "ClusterStatus"
}

<#
.SYNOPSIS
Attempt to force the completion of a currently running Safeguard cluster operation via the Safeguard Web API.

.DESCRIPTION
This cmdlet can be used to force a cluster operation to complete that is not being acknowledged by all
appliances in the cluster. This is not always possible with the current connection. You may need to
connect to a different appliance in the cluster to perform this operation depending on which appliance is
having trouble.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.PARAMETER Timeout
A timeout value in seconds for unlocking cluster (default: 600s or 10m)

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Unlock-SafeguardCluster -AccessToken $token -Appliance 10.5.33.144

.EXAMPLE
Unlock-SafeguardCluster
#>

function Unlock-SafeguardCluster
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$false)]
        [int]$Timeout = 600
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    $local:OpStatus = (Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core GET Cluster/Status -RetryUrl "ClusterStatus")
    if ($local:OpStatus.Operation -eq "None")
    {
        Write-Host "No cluster operation is currently running."
        $local:OpStatus
    }
    else
    {
        Import-Module -Name "$PSScriptRoot\sg-utilities.psm1" -Scope Local
        Write-Host "Attempting to force completion of $($local:OpStatus.Operation) operation..."
        Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core POST "Cluster/Status/ForceComplete" `
            -RetryUrl "ClusterStatus/ForceComplete"
        Wait-ForClusterOperation -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure -Timeout $Timeout
    }
}

<#
.SYNOPSIS
Get summary of information about the Safeguard cluster via the Safeguard Web API.

.DESCRIPTION
This cmdlet will report on the current cluster primary and all of the members. It will
report on any errors currently found in cluster health as well as the status of any
on-going cluster operations. All information is taken from the perspective of the
connected appliance. All of the health information is reported from the cache.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Get-SafeguardClusterSummary -AccessToken $token -Appliance 10.5.33.144

.EXAMPLE
Get-SafeguardClusterSummary
#>

function Get-SafeguardClusterSummary
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure
    )

    $ApplianceId = (Invoke-SafeguardMethod -Anonymous -Appliance $Appliance -Insecure:$Insecure Notification GET Status).ApplianceId
    Write-Host "Cluster Health Summary from perspective of Appliance ID: $ApplianceId"

    Write-Host "`n---Primary---"
    Write-Host (
    Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core GET "Cluster/Members" `
            -Parameters @{fields = "Id,Name,Ipv4Address,Ipv6Address"
                          filter = "IsLeader eq true"} -RetryUrl "ClusterMembers" | Format-Table | Out-String)

    Write-Host "---Cluster---"
    $local:Members = Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core GET "Cluster/Members" `
                             -Parameters @{fields = "Id,Name,Ipv4Address,Ipv6Address,Health,EnrolledSince"} -RetryUrl "ClusterMembers"
    $local:Errors = @()
    $local:Timestamps = @()
    $local:Reachable = (Get-ReachableMatrix $local:Members)
    Write-Host (
    $local:Members | ForEach-Object {
        $local:Object = (New-Object -TypeName PSObject -Property @{
            Id = $_.Id
            Name = $_.Name
            State = $_.Health.State
            Ipv4Address = $_.Ipv4Address
            Ipv6Address = $_.Ipv6Address
        })
        $local:Timestamps += (New-Object -TypeName PSObject -Property @{
            Id = $_.Id
            Name = $_.Name
            LocalTimeWhenRun = [System.TimeZone]::CurrentTimeZone.ToLocalTime($_.Health.CheckDate).ToString("yyyy-MM-ddTHH:mm:ss")
        })
        if (-not ($_.EnrolledSince))
        {
            $local:Object | Add-Member -MemberType NoteProperty -Name "Communication" -Value "Not Enrolled"
            $local:Object | Add-Member -MemberType NoteProperty -Name "Connectivity" -Value "Not Enrolled"
            $local:Object | Add-Member -MemberType NoteProperty -Name "Workflow" -Value "Not Enrolled"
            $local:Object | Add-Member -MemberType NoteProperty -Name "Policy" -Value "Not Enrolled"
            $local:Object | Add-Member -MemberType NoteProperty -Name "Sessions" -Value "Not Enrolled"
            $local:Object
            return # <-- equivalent to continue for ForEach-Object script block
        }
        if ($_.Health.ClusterCommunication.Status -eq "Healthy")
        {
            $local:Object | Add-Member -MemberType NoteProperty -Name "Communication" -Value "$([Char]8730)"
        }
        else
        {
            $local:Object | Add-Member -MemberType NoteProperty -Name "Communication" -Value "$($_.Health.ClusterCommunication.Status)"
            $local:Errors += (Get-ClusterHealthError $_.Id $_.Name "Cluster Communication" $_.Health.State $_.Health.ClusterCommunication)
        }
        if ($_.Health.ClusterConnectivity.Status -eq "Healthy")
        {
            $local:Object | Add-Member -MemberType NoteProperty -Name "Connectivity" -Value "$([Char]8730)"
        }
        else
        {
            $local:Object | Add-Member -MemberType NoteProperty -Name "Connectivity" -Value "$($_.Health.ClusterConnectivity.Status)"
            $local:Errors += (Get-ClusterHealthError $_.Id $_.Name "Cluster Connectivity" $_.Health.State $_.Health.ClusterConnectivity)
        }
        if ($_.Health.AccessWorkflow.Status -eq "Healthy")
        {
            $local:Object | Add-Member -MemberType NoteProperty -Name "Workflow" -Value "$([Char]8730)"
        }
        else
        {
            $local:Object | Add-Member -MemberType NoteProperty -Name "Workflow" -Value "$($_.Health.AccessWorkflow.Status)"
            $local:Errors += (Get-ClusterHealthError $_.Id $_.Name "Access Workflow" $_.Health.State $_.Health.AccessWorkflow)
        }
        if ($_.Health.PolicyData.Status -eq "Healthy")
        {
            $local:Object | Add-Member -MemberType NoteProperty -Name "Policy" -Value "$([Char]8730)"
        }
        else
        {
            $local:Object | Add-Member -MemberType NoteProperty -Name "Policy" -Value "$($_.Health.PolicyData.Status)"
            $local:Errors += (Get-ClusterHealthError $_.Id $_.Name "Policy Data" $_.Health.State $_.Health.PolicyData)
        }
        if ($_.Health.SessionsModule.Status -eq "Healthy")
        {
            $local:Object | Add-Member -MemberType NoteProperty -Name "Sessions" -Value "$([Char]8730)"
        }
        else
        {
            $local:Object | Add-Member -MemberType NoteProperty -Name "Sessions" -Value "$($_.Health.SessionsModule.Status)"
            $local:Errors += (Get-ClusterHealthError $_.Id $_.Name "Sessions Module" $_.Health.State $_.Health.SessionsModule)
        }
        $local:Object
    } | Format-Table Id,Name,State,Ipv4Address,Ipv6Address,Communication,Connectivity,Workflow,Policy,Sessions -AutoSize | Out-String)

    Write-Host "---Cluster Health Check Timestamp---"
    Write-Host(
    $local:Timestamps | Format-Table Id,Name,LocalTimeWhenRun | Out-String)

    Write-Host "---Cluster Member Reachability---"
    Write-Host(
    $local:Reachable | Format-Table | Out-String)

    Write-Host "---Cluster Errors---`n"
    if (-not ($local:Errors))
    {
        $local:Errors += "None"
    }
    Write-Host(
    $local:Errors | Out-String)

    Write-Host "`n---Cluster Operation Status---"
    Write-Host(
    Get-SafeguardClusterOperationStatus -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure | Format-Table | Out-String)
}

<#
.SYNOPSIS
Get platform task load information from Safeguard via the Web API.

.DESCRIPTION
Retrieve appliance-specific information about queued platform tasks from the Web API.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Get-SafeguardClusterPlatformTaskLoadStatus -AccessToken $SafeguardSession.AccessToken -Appliance 10.5.32.54

.EXAMPLE
Get-SafeguardClusterPlatformTaskLoadStatus
#>

function Get-SafeguardClusterPlatformTaskLoadStatus
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    # TODO: Switch this to use fields when the API supports it
    (Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core `
        GET "Cluster/Status/PlatformTaskLoadStatus").ApplianceLoadData
}

<#
.SYNOPSIS
Get platform task queue information from Safeguard via the Web API.

.DESCRIPTION
Retrieve cluster-wide information about queued platform tasks from the Web API.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Get-SafeguardClusterPlatformTaskQueueStatus -AccessToken $SafeguardSession.AccessToken -Appliance 10.5.32.54

.EXAMPLE
Get-SafeguardClusterPlatformTaskQueueStatus
#>

function Get-SafeguardClusterPlatformTaskQueueStatus
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    # TODO: Switch this to use fields when the API supports it
    (Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Core `
        GET "Cluster/Status/PlatformTaskLoadStatus") | Select-Object -Property * -ExcludeProperty "ApplianceLoadData"
}

<#
.SYNOPSIS
Get cluster members with VPN IPv6 address from Safeguard via the Web API.

.DESCRIPTION
Retrieve the list of Safeguard appliances in this cluster from the Web API
and calculate the VPN IPv6 address.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.PARAMETER Member
A string containing an ID, name, or network address for the member appliance.

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Get-SafeguardClusterVpnIpv6Address
#>

function Get-SafeguardClusterVpnIpv6Address
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$false,Position=0)]
        [string]$Member
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    Import-Module -Name "$PSScriptRoot\sg-utilities.psm1" -Scope Local
    (Get-SafeguardClusterMember -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure -Member $Member) | ForEach-Object {
        $local:Vpn = Get-VpnIpv6Address $_.Id
        New-Object PSObject -Property ([ordered]@{
            ApplianceName = $_.Name;
            ApplianceId = $_.Id;
            IsPrimary = $_.IsLeader;
            Ipv4Address = $_.Ipv4Address;
            Ipv6Address = $_.Ipv6Address;
            VpnIpv6Address = $local:Vpn
        })
    }
}

<#
.SYNOPSIS
Test VPN throughput using the Safeguard Web API.

.DESCRIPTION
This cmdlet will test VPN throughput from one appliance to another in
the cluster.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.PARAMETER TargetMember
A string containing an identifier or name of the target appliance.

.PARAMETER Megabytes
An integer of the number of megabytes to send in the test.

.PARAMETER Raw
Show raw API output rather than returning an object.

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Invoke-SafeguardMemberThroughput -TargetMember 10.5.5.5

.EXAMPLE
Invoke-SafeguardMemberThroughput -TargetMember SG-AC1F6B18BAB6

.EXAMPLE
Invoke-SafeguardMemberThroughput -TargetMember AC1F6B18BAB6 -Raw -Megabytes 1024
#>

function Invoke-SafeguardMemberThroughput
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$true,Position=0)]
        [string]$TargetMember,
        [Parameter(Mandatory=$false)]
        [int]$Megabytes = 100,
        [Parameter(Mandatory=$false)]
        [switch]$Raw
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    $local:Target = (Get-SafeguardClusterMember -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure -Member $TargetMember)
    if (-not $local:Target)
    {
        throw "Cluster member '$TargetMember' not found"
    }
    $local:Output = (Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Appliance POST `
                        "NetworkDiagnostics/Throughput" -Timeout 1800 -Body @{
                            TargetApplianceId = $local:Target.Id;
                            MbToTransfer = $Megabytes
                        })
    $local:Found = $local:Output -match ".*Throughput: ([\d.]+)"
    if (-not $local:Found -or $Raw)
    {
        $local:Output
    }
    else
    {
        $local:Source = (Get-SafeguardStatus -Appliance $Appliance -Insecure:$Insecure)
        $local:MBytesPerSec = [decimal]$matches[1]
        New-Object PSObject -Property ([ordered]@{
            SourceApplianceName = $local:Source.ApplianceName;
            SourceApplianceId = $local:Source.ApplianceId;
            TargetApplianceName = $local:Target.Name;
            TargetApplianceId = $local:Target.Id;
            MegabytesPerSecond = $local:MBytesPerSec;
            MegabitsPerSecond = ($local:MBytesPerSec * 8)
        })
    }
}

<#
.SYNOPSIS
Test VPN throughput for the entire cluster using the Safeguard Web API.

.DESCRIPTION
This cmdlet will test VPN throughput from all appliances to every other
appliance in the cluster.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.PARAMETER Megabytes
An integer of the number of megabytes to send in the test.

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Invoke-SafeguardClusterThroughput

.EXAMPLE
Invoke-SafeguardClusterThroughput -Megabytes 10
#>

function Invoke-SafeguardClusterThroughput
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$false)]
        [int]$Megabytes = 100
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    $local:Members = (Get-SafeguardClusterMember -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure)

    if (-not $AccessToken -and $SafeguardSession)
    {
        $AccessToken = $SafeguardSession["AccessToken"]
    }
    if (-not $Appliance -and $SafeguardSession)
    {
        # if using session variable also inherit trust status
        $Insecure = $SafeguardSession["Insecure"]
    }

    $local:Members | ForEach-Object {
        $local:Source = $_
        if ($local:Source.Ipv4Address)
        {
            $local:SourceAppliance = $local:Source.Ipv4Address
        }
        else
        {
            $local:SourceAppliance = $local:Source.Ipv6Address
        }
        $local:Members | ForEach-Object {
            $local:Target = $_
            if ($local:Source.Id -ne $local:Target.Id)
            {
                Invoke-SafeguardMemberThroughput -Appliance $local:SourceAppliance -AccessToken $AccessToken -Insecure:$Insecure `
                    -TargetMember $local:Target.Id -Megabytes $Megabytes
            }
        }
    }
}

<#
.SYNOPSIS
Test ping latency using the Safeguard Web API.

.DESCRIPTION
This cmdlet will test ping latency from one appliance to another in
the cluster.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.PARAMETER TargetMember
A string containing an identifier or name of the target appliance.

.PARAMETER Count
An integer of the number of echo requests to send.

.PARAMETER Size
An integer containing the size of the packet to send.

.PARAMETER NoFrag
Whether or not to allow packet fragmentation.

.PARAMETER Raw
Show raw API output rather than returning an object.

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Invoke-SafeguardMemberPing -TargetMember 10.5.5.5

.EXAMPLE
Invoke-SafeguardMemberPing -TargetMember SG-AC1F6B18BAB6

.EXAMPLE
Invoke-SafeguardMemberPing AC1F6B18BAB6 -Size 1200 -NoFrag -Count 1
#>

function Invoke-SafeguardMemberPing
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure,
        [Parameter(Mandatory=$true,Position=0)]
        [string]$TargetMember,
        [Parameter(Mandatory=$false)]
        [int]$Count = 4,
        [Parameter(Mandatory=$false)]
        [int]$Size = 0,
        [Parameter(Mandatory=$false)]
        [switch]$NoFrag,
        [Parameter(Mandatory=$false)]
        [switch]$Raw
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    $local:Target = (Get-SafeguardClusterVpnIpv6Address -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure -Member $TargetMember)

    $local:Timeout = 300
    if (($Count * 3) -gt $local:Timeout)
    {
        $local:Timeout = ($Count * 3)
    }

    $local:Body = @{
        NetworkAddress = $local:Target.VpnIpv6Address;
        NumberEchoRequests = $Count
    }

    if ($Size -gt 0) { $local:Body["BufferSize"] = $Size }
    if ($NoFrag) { $local:Body["DontFragmentFlag"] = $true }

    $local:Output = (Invoke-SafeguardMethod -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure Appliance POST NetworkDiagnostics/Ping `
                        -Timeout $local:Timeout -Body $local:Body)
    $local:Found = $local:Output -match ".*Average = ([\d.]+)ms"
    if (-not $local:Found -or $Raw)
    {
        $local:Output
    }
    else
    {
        $local:Source = (Get-SafeguardStatus -Appliance $Appliance -Insecure:$Insecure)
        $local:Milliseconds = [decimal]$matches[1]
        New-Object PSObject -Property ([ordered]@{
            SourceApplianceName = $local:Source.ApplianceName;
            SourceApplianceId = $local:Source.ApplianceId;
            TargetApplianceName = $local:Target.ApplianceName;
            TargetApplianceId = $local:Target.ApplianceId;
            Milliseconds = $local:Milliseconds
        })
    }
}

<#
.SYNOPSIS
Test ping latency for the entire cluster using the Safeguard Web API.

.DESCRIPTION
This cmdlet will test ping latency from all appliances to every other
appliance in the cluster.

.PARAMETER Appliance
IP address or hostname of a Safeguard appliance.

.PARAMETER AccessToken
A string containing the bearer token to be used with Safeguard Web API.

.PARAMETER Insecure
Ignore verification of Safeguard appliance SSL certificate.

.INPUTS
None.

.OUTPUTS
JSON response from Safeguard Web API.

.EXAMPLE
Invoke-SafeguardClusterPing
#>

function Invoke-SafeguardClusterPing
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)]
        [string]$Appliance,
        [Parameter(Mandatory=$false)]
        [object]$AccessToken,
        [Parameter(Mandatory=$false)]
        [switch]$Insecure
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    $local:Members = (Get-SafeguardClusterMember -AccessToken $AccessToken -Appliance $Appliance -Insecure:$Insecure)

    if (-not $AccessToken -and $SafeguardSession)
    {
        $AccessToken = $SafeguardSession["AccessToken"]
    }
    if (-not $Appliance -and $SafeguardSession)
    {
        # if using session variable also inherit trust status
        $Insecure = $SafeguardSession["Insecure"]
    }

    $local:Members | ForEach-Object {
        $local:Source = $_
        if ($local:Source.Ipv4Address)
        {
            $local:SourceAppliance = $local:Source.Ipv4Address
        }
        else
        {
            $local:SourceAppliance = $local:Source.Ipv6Address
        }
        $local:Members | ForEach-Object {
            $local:Target = $_
            if ($local:Source.Id -ne $local:Target.Id)
            {
                Invoke-SafeguardMemberPing -Appliance $local:SourceAppliance -AccessToken $AccessToken -Insecure:$Insecure `
                    -TargetMember $local:Target.Id
            }
        }
    }
}