Functions/Task/SqlServerAvailabilityGroupFailover.ps1

<#
    .SYNOPSIS
        Autonance DSL task to failover an SQL Server Availability Group.
 
    .DESCRIPTION
        Use this task to failover the specified SQL Server Availability Group to
        the specified computer and instance. It will use the SQLPS module on the
        remote system.
 
    .NOTES
        Author : Claudio Spizzi
        License : MIT License
 
    .LINK
        https://github.com/claudiospizzi/Autonance
#>

function SqlServerAvailabilityGroupFailover
{
    [CmdletBinding()]
    param
    (
        # This is the target Windows computer for the planned failover.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $ComputerName,

        # Target SQL instance for the planned planned.
        [Parameter(Mandatory = $true, Position = 1)]
        [System.String]
        $SqlInstance,

        # The availability group name to perform a planned manual failover.
        [Parameter(Mandatory = $true, Position = 2)]
        [System.String]
        $AvailabilityGroup,

        # Specifies a user account that has permission to perform the task.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # Specifies the number of retries between availability group state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Count = 60,

        # Specifies the interval between availability group state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Delay = 2
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'SqlServerAvailabilityGroupFailover task not encapsulated in a Maintenance container'
    }

    New-AutonanceTask -Type 'SqlServerAvailabilityGroupFailover' -Name "$ComputerName $SqlInstance $AvailabilityGroup" -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # This is the target Windows computer for the planned failover.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.String]
            $ComputerName,

            # Target SQL instance for the planned planned.
            [Parameter(Mandatory = $true, Position = 1)]
            [System.String]
            $SqlInstance,

            # The availability group name to perform a planned manual failover.
            [Parameter(Mandatory = $true, Position = 2)]
            [System.String]
            $AvailabilityGroup,

            # Specifies a user account that has permission to perform the task.
            [Parameter(Mandatory = $false)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential,

            # Specifies the number of retries between availability group state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Count = 60,

            # Specifies the interval between availability group state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Delay = 2
        )

        $SqlInstancePath = $SqlInstance

        # Default MSSQLSERVER instance, only specified as server name
        if (-not $SqlInstance.Contains('\'))
        {
            $SqlInstancePath = "$SqlInstance\DEFAULT"
        }

        try
        {
            ## Part 1 - Connect to the SQL Server and load the SQLPS module

            $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType WinRM -ErrorAction Stop

            # Load the SQL PowerShell module but suppress warnings because of
            # uncommon cmdlet verbs.
            Invoke-Command -Session $session -ScriptBlock { Import-Module -Name 'SQLPS' -WarningAction 'SilentlyContinue' } -ErrorAction Stop


            ## Part 2 - Check the current role and state

            $replicas = Invoke-Command -Session $session -ScriptBlock { Get-ChildItem -Path "SQLSERVER:\Sql\$using:SqlInstancePath\AvailabilityGroups\$using:AvailabilityGroup\AvailabilityReplicas" | Select-Object * } -ErrorAction Stop
            $replica  = $replicas.Where({$_.Name -eq $SqlInstance})[0]


            ## Part 3 - Planned manual failover

            if ($replica.Role -ne 'Primary')
            {
                ## Part 3a - Check replicate state

                Write-Autonance -Message "Replica role is $($replica.Role.ToString().ToLower())"
                Write-Autonance -Message "Replica state is $($replica.RollupRecoveryState.ToString().ToLower()) and $($replica.RollupSynchronizationState.ToString().ToLower())"

                if ($replica.RollupRecoveryState.ToString() -ne 'Online' -or $replica.RollupSynchronizationState.ToString() -ne 'Synchronized')
                {
                    throw 'Replicate is not ready for planned manual failover!'
                }


                ## Part 3b - Invoke failover

                Write-Autonance -Message "Failover $AvailabilityGroup to $SqlInstance ..."

                Invoke-Command -Session $session -ScriptBlock { Switch-SqlAvailabilityGroup -Path "SQLSERVER:\Sql\$using:SqlInstancePath\AvailabilityGroups\$using:AvailabilityGroup" } -ErrorAction Stop

                Wait-AutonanceTask -Activity "$SqlInstance replication is restoring ..." -Count $Count -Delay $Delay -Condition {

                    $replicas = Invoke-Command -Session $session -ScriptBlock { Get-ChildItem -Path "SQLSERVER:\Sql\$using:SqlInstancePath\AvailabilityGroups\$using:AvailabilityGroup\AvailabilityReplicas" | ForEach-Object { $_.Refresh(); $_ } } -ErrorAction Stop

                    $condition = $true

                    # Check all replica states
                    foreach ($replica in $replicas)
                    {
                        # Test for primary replica
                        if ($replica.Name -eq $SqlInstance)
                        {
                            $condition = $condition -and $replica.Role -eq 'Primary'
                            $condition = $condition -and $replica.RollupRecoveryState -eq 'Online'
                        }

                        # Test for any replica
                        $condition = $condition -and $replica.RollupSynchronizationState -eq 'Synchronized'
                    }

                    $condition
                }
            }


            ## Part 4 - Verify

            $replicas = Invoke-Command -Session $session -ScriptBlock { Get-ChildItem -Path "SQLSERVER:\Sql\$using:SqlInstancePath\AvailabilityGroups\$using:AvailabilityGroup\AvailabilityReplicas" } -ErrorAction Stop
            $replica  = $replicas.Where({$_.Name -eq $SqlInstance})[0]

            Write-Autonance -Message "Replica role is $($replica.Role.ToString().ToLower())"
            Write-Autonance -Message "Replica state is $($replica.RollupRecoveryState.ToString().ToLower()) and $($replica.RollupSynchronizationState.ToString().ToLower())"

            if ($replica.Role -ne 'Primary')
            {
                throw 'Replica role is not primary'
            }

            if ($replica.RollupRecoveryState -ne 'Online')
            {
                throw 'Replica recovery state is not online'
            }

            if ($replica.RollupSynchronizationState -ne 'Synchronized')
            {
                throw 'Replica synchronization state is not synchronized'
            }
        }
        catch
        {
            throw $_
        }
        finally
        {
            Remove-AutonanceSession -Session $session

            # Ensure, that the next task has a short delay
            Start-Sleep -Seconds 3
        }
    }
}