Private/Get-sqmDatabaseAgMembership.ps1

<#
.SYNOPSIS
    Ermittelt verlaesslich, ob eine Datenbank Mitglied einer AlwaysOn-Availability-Group ist.
 
.DESCRIPTION
    Beantwortet DREI Zustaende statt zwei, und genau darin liegt der Zweck dieser Funktion:
 
        - Die Instanz hat AlwaysOn gar nicht aktiviert -> HadrEnabled = $false
        - Die Instanz hat AlwaysOn, die Datenbank ist kein Mitglied -> IsAgDatabase = $false
        - Die Frage konnte nicht beantwortet werden -> Ausnahme
 
    Der bisherige Weg ueber Get-DbaAgDatabase mit -ErrorAction SilentlyContinue konnte den dritten
    Zustand nicht ausdruecken: fehlende Rechte (VIEW ANY DEFINITION / VIEW SERVER STATE), eine
    stolpernde SMO-Enumeration oder eine Verbindung zur falschen Instanz lieferten allesamt ein
    leeres Ergebnis, das vom aufrufenden Code als "ist in keiner AG" gelesen wurde. Invoke-sqm-
    RestoreDatabase ist daraufhin direkt auf ALTER DATABASE und RESTORE losgegangen, die SQL Server
    beide mit "is involved in a database mirroring session or an availability group" abgelehnt hat -
    nachdem bereits mehrere Schritte gelaufen waren. Ein stiller Fallback auf die harmlos aussehende
    Variante, an der genau die Sicherung haengt, die den Restore ueberhaupt erst moeglich macht.
 
    Gefragt wird direkt in den Katalogsichten, nicht ueber SMO:
 
        sys.availability_databases_cluster Mitgliedschaft, clusterweit von JEDEM Replikat lesbar
        sys.dm_hadr_availability_replica_states aktuelle Rollenverteilung, fuer die Primary
 
    Beide sind reine Metadaten und kosten nichts. Die Mitgliedschaftsfrage ist die harte: schlaegt
    sie fehl, wirft diese Funktion. Die Primary-Ermittlung ist die weiche: sie braucht zusaetzlich
    VIEW SERVER STATE, und wenn die fehlt, bleibt PrimaryReplica leer, ohne dass die Mitgliedschafts-
    aussage dadurch wertlos wuerde - der Aufrufer kann die Primary dann anderweitig bestimmen.
 
.PARAMETER SqlInstance
    Instanz, ueber die gefragt wird. Die AG-Katalogsichten sind clusterweit, es muss also nicht die
    Primary sein.
 
.PARAMETER Database
    Name der Datenbank.
 
.PARAMETER SqlCredential
    Optionales PSCredential.
 
.OUTPUTS
    PSCustomObject mit HadrEnabled, IsAgDatabase, AvailabilityGroupName, PrimaryReplica.
 
.NOTES
    Private Hilfsfunktion fuer Invoke-sqmRestoreDatabase - nicht exportiert.
#>

function Get-sqmDatabaseAgMembership
{
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$SqlInstance,
        [Parameter(Mandatory = $true)]
        [string]$Database,
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$SqlCredential
    )

    $functionName = $MyInvocation.MyCommand.Name

    $connParams = @{ SqlInstance = $SqlInstance; Database = 'master' }
    if ($SqlCredential) { $connParams['SqlCredential'] = $SqlCredential }

    # ---- 1. Ist AlwaysOn auf dieser Instanz ueberhaupt aktiviert? ----
    # Auf einer Instanz ohne HADR sind die AG-Sichten zwar vorhanden, aber leer. Diese Abfrage
    # trennt "keine AG vorhanden" sauber von "AG vorhanden, Datenbank nicht drin" und laeuft auf
    # jeder Edition.
    try
    {
        $hadrRow = Invoke-DbaQuery @connParams -Query "SELECT CONVERT(int, ISNULL(SERVERPROPERTY('IsHadrEnabled'), 0)) AS HadrEnabled" `
                                         -As PSObject -EnableException
        $hadrEnabled = [int]$hadrRow.HadrEnabled -eq 1
    }
    catch
    {
        throw "AlwaysOn-Status von '$SqlInstance' konnte nicht ermittelt werden: $($_.Exception.Message)"
    }

    if (-not $hadrEnabled)
    {
        Invoke-sqmLogging -Message "Instanz '$SqlInstance' hat AlwaysOn nicht aktiviert - '$Database' kann keine AG-Datenbank sein." `
                          -FunctionName $functionName -Level 'INFO'
        return [PSCustomObject]@{
            HadrEnabled              = $false
            IsAgDatabase          = $false
            AvailabilityGroupName = $null
            PrimaryReplica          = $null
        }
    }

    # ---- 2. Mitgliedschaft - die harte Frage, ein Fehler hier darf NICHT als "nein" durchgehen ----
    $dbLiteral = $Database.Replace("'", "''")
    $membershipQuery = @"
SELECT TOP (1) ag.name AS AvailabilityGroupName
FROM sys.availability_databases_cluster adc
JOIN sys.availability_groups ag ON ag.group_id = adc.group_id
WHERE adc.database_name = N'$dbLiteral';
"@


    try
    {
        $agRow = @(Invoke-DbaQuery @connParams -Query $membershipQuery -As PSObject -EnableException) | Select-Object -First 1
    }
    catch
    {
        throw "AG-Mitgliedschaft von '$Database' auf '$SqlInstance' konnte nicht ermittelt werden - der Vorgang wird abgebrochen, " +
        "statt die Datenbank faelschlich als Standalone zu behandeln (ein RESTORE gegen eine AG-Datenbank wird von SQL Server " +
        "abgelehnt). Fehlen ggf. die Rechte VIEW ANY DEFINITION? Ursache: $($_.Exception.Message)"
    }

    if (-not $agRow -or [string]::IsNullOrWhiteSpace($agRow.AvailabilityGroupName))
    {
        Invoke-sqmLogging -Message "'$Database' ist auf '$SqlInstance' in keiner Availability Group (Instanz hat AlwaysOn aktiviert)." `
                          -FunctionName $functionName -Level 'INFO'
        return [PSCustomObject]@{
            HadrEnabled              = $true
            IsAgDatabase          = $false
            AvailabilityGroupName = $null
            PrimaryReplica          = $null
        }
    }

    $agName = [string]$agRow.AvailabilityGroupName

    # ---- 3. Primary - die weiche Frage, braucht zusaetzlich VIEW SERVER STATE ----
    # Faellt sie aus, bleibt die Mitgliedschaftsaussage oben trotzdem gueltig; der Aufrufer
    # ermittelt die Primary dann ueber das SMO-AG-Objekt.
    $primaryReplica = $null
    $primaryQuery = @"
SELECT TOP (1) ar.replica_server_name AS PrimaryReplica
FROM sys.dm_hadr_availability_replica_states ars
JOIN sys.availability_replicas ar ON ar.replica_id = ars.replica_id
JOIN sys.availability_groups ag ON ag.group_id = ars.group_id
WHERE ag.name = N'$($agName.Replace("'", "''"))' AND ars.role = 1;
"@

    try
    {
        $primaryRow = @(Invoke-DbaQuery @connParams -Query $primaryQuery -As PSObject -EnableException) | Select-Object -First 1
        if ($primaryRow) { $primaryReplica = [string]$primaryRow.PrimaryReplica }
    }
    catch
    {
        Invoke-sqmLogging -Message "Primary-Replikat der AG '$agName' konnte nicht ermittelt werden (fehlt VIEW SERVER STATE?), die Mitgliedschaft steht aber fest: $($_.Exception.Message)" `
                          -FunctionName $functionName -Level 'WARNING'
    }

    Invoke-sqmLogging -Message "'$Database' ist Mitglied der Availability Group '$agName'$(if ($primaryReplica) { ", Primary: '$primaryReplica'" })." `
                      -FunctionName $functionName -Level 'INFO'

    return [PSCustomObject]@{
        HadrEnabled              = $true
        IsAgDatabase          = $true
        AvailabilityGroupName = $agName
        PrimaryReplica          = $primaryReplica
    }
}