Private/Invoke-QAFrameworkFailoverSwitch.ps1

function Get-QAFrameworkFailoverStateValue {
    <#
    .SYNOPSIS
        Maps a QAFramework execution phase onto the FailoverState value in SystemSettings.json.
    .DESCRIPTION
        The legacy FailoverTestState enum uses BeforeSwitch = 0, AfterSwitch = 10 and
        AfterSwitchBack = 20. The runner rewrites this value on every agent before it starts a
        failover phase, and the test framework on the agent uses it to decide which of the
        BeforeSwitch, AfterSwitch and AfterSwitchBack methods to run.
    .PARAMETER Phase
        The execution plan phase.
    #>

    [CmdletBinding()]
    [OutputType([int])]
    param(
        [Parameter(Mandatory = $true)][string]$Phase
    )

    switch -Regex ($Phase) {
        '^(?i)Failover(Direct)?AfterSwitchBack$' { return 20 }
        '^(?i)Failover(Direct)?AfterSwitch$' { return 10 }
        default { return 0 }
    }
}

function Set-QAFrameworkFailoverState {
    <#
    .SYNOPSIS
        Rewrites the FailoverState in SystemSettings.json on every DataMiner agent.
    .DESCRIPTION
        Runs the agent script with -SkipDependencies through a bridge execution, so only the
        settings file changes and the harvested dependencies stay in place.
    .PARAMETER Topology
        A cluster topology from Get-QAFrameworkClusterTopology.
    .PARAMETER State
        The FailoverState value: 0 before the switch, 10 after the switch, 20 after switching back.
    .PARAMETER Configuration
        A run configuration, for rtManagerRoot and systemSettings.
    .PARAMETER TestPackageContentPath
        The test package content root on the agents.
    .PARAMETER ScriptPath
        The agent script path relative to the content root.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true)][object]$Topology,
        [Parameter(Mandatory = $true)][int]$State,
        [Parameter()][object]$Configuration,
        [Parameter()][AllowNull()][AllowEmptyString()][string]$TestPackageContentPath,
        [Parameter()][string]$ScriptPath = 'TestPackagePipeline/helpers/Initialize-QAFrameworkAgent.ps1'
    )

    $parameters = @{
        Topology         = $Topology
        FailoverState    = $State
        SkipDependencies = $true
        ScriptPath       = $ScriptPath
        ContinueOnError  = $true
    }
    if ($Configuration) { $parameters['Configuration'] = $Configuration }
    if (-not [string]::IsNullOrWhiteSpace($TestPackageContentPath)) { $parameters['TestPackageContentPath'] = $TestPackageContentPath }

    return Initialize-QAFrameworkAgents @parameters
}

function Invoke-QAFrameworkFailoverSwitch {
    <#
    .SYNOPSIS
        Switches every failover pair in the cluster and waits until both agents are back online.
    .DESCRIPTION
        Wraps Start-QAOpsFailoverSwitch and Wait-QAOpsFailoverSwitch. All pairs are switched at
        the same time and awaited together, because a large cluster would otherwise spend the
        agents-online wait once per pair.
    .PARAMETER Topology
        A cluster topology from Get-QAFrameworkClusterTopology.
    .PARAMETER TimeoutSeconds
        How long a switch may take. Defaults to 900 seconds, the legacy agents-online wait.
    .OUTPUTS
        One result object per pair with PairId, Success and Message.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true)][object]$Topology,
        [Parameter()][int]$TimeoutSeconds = 900
    )

    foreach ($command in @('Start-QAOpsFailoverSwitch', 'Wait-QAOpsFailoverSwitch')) {
        if (-not (Get-Command -Name $command -ErrorAction SilentlyContinue)) {
            throw "$command is not available. Import the QAOps.PowerShell module before running the failover phases."
        }
    }

    $pairs = [object[]]@($Topology.FailoverPairs)
    if ($pairs.Count -eq 0) { return }

    $switches = [System.Collections.Generic.List[object]]::new()

    foreach ($pair in $pairs) {
        try {
            $operation = Start-QAOpsFailoverSwitch -Bridge $pair.Primary.BridgeId -PairId $pair.PairId
            $switches.Add([pscustomobject]@{ Pair = $pair; Operation = $operation })
        }
        catch {
            Write-Warning "Could not start the failover switch of pair '$($pair.PairId)': $($_.Exception.Message)"
            [pscustomobject]@{ PairId = $pair.PairId; Success = $false; ActiveBridgeId = $null; Message = $_.Exception.Message }
        }
    }

    foreach ($entry in $switches) {
        try {
            $result = Wait-QAOpsFailoverSwitch -Switch $entry.Operation -TimeoutSeconds $TimeoutSeconds
            $success = [string]::Equals([string]$result.State, 'Completed', [StringComparison]::OrdinalIgnoreCase)

            [pscustomobject]@{
                PairId         = $entry.Pair.PairId
                Success        = $success
                # The same Bridge owns the operation. The response identifies the DataMiner
                # agent that became online by name, not by another Bridge id.
                ActiveBridgeId = if ($success) { $entry.Pair.Primary.BridgeId } else { $null }
                Message        = if ($success) { "Active agent is now '$($result.CurrentOnlineAgentName)'." } else { [string]$result.ErrorMessage }
            }
        }
        catch {
            Write-Warning "The failover switch of pair '$($entry.Pair.PairId)' did not finish: $($_.Exception.Message)"
            [pscustomobject]@{ PairId = $entry.Pair.PairId; Success = $false; ActiveBridgeId = $null; Message = $_.Exception.Message }
        }
    }
}