Public/Repair-sqmServerName.ps1

<#
.SYNOPSIS
    Gleicht den in SQL Server registrierten Servernamen (@@SERVERNAME) an den aktuellen
    Windows-Hostnamen/FQDN an, nachdem der Rechner umbenannt wurde.
 
.DESCRIPTION
    Nach einer Windows-Umbenennung (Hostname- oder FQDN-Aenderung, z. B. nach VM-Klon oder
    Rechenzentrums-Umzug) meldet Windows sofort den neuen Namen (SERVERPROPERTY('MachineName')),
    waehrend SQL Server den ALTEN Namen weiter in sys.servers als "lokalen Server" fuehrt
    (SERVERPROPERTY('ServerName') / @@SERVERNAME). Dieser Drift faellt oft erst auf, wenn
    Replikation, Linked-Server-Loopbacks, SSRS oder Zertifikate mit dem falschen Namen
    fehlschlagen.
 
    Diese Funktion:
    - Ermittelt den erwarteten Namen aus MachineName (+ \InstanceName bei Named Instance) und
      vergleicht ihn mit dem registrierten @@SERVERNAME.
    - Bricht bei Abweichung NICHT blind mit sp_dropserver/sp_addserver durch, sondern prueft
      vorher Failover-Cluster-Mitgliedschaft (IsClustered), AlwaysOn-AG-Mitgliedschaft
      (sys.availability_replicas) und Replikationsrollen (sys.databases: is_distributor,
      is_published, is_subscribed, is_merge_published). In all diesen Faellen haengen andere
      Objekte am alten Namen (replica_server_name, Distributor-Registrierung, Publikationen) -
      ein reines sp_addserver wuerde die Instanz technisch umbenennen, aber diese Referenzen
      brechen. Ohne -Force wird deshalb abgebrochen (Status 'Blocked').
    - Fuehrt bei Freigabe sp_dropserver (alter Name) + sp_addserver (neuer Name, local) aus.
    - Weist darauf hin, dass ein Neustart des SQL Server-Dienstes noetig ist, bis @@SERVERNAME
      den neuen Wert zurueckgibt. Die Funktion startet den Dienst NICHT automatisch neu.
 
.PARAMETER SqlInstance
    Name der SQL-Instanz. Standard: lokaler Computername ($env:COMPUTERNAME).
 
.PARAMETER SqlCredential
    Optionale SQL-Credentials (Standard: Windows-Authentifizierung).
 
.PARAMETER Force
    Ignoriert die Guard-Checks (Cluster/AG/Replikation) und erzwingt sp_dropserver/sp_addserver
    trotzdem. Nur verwenden, wenn die betroffenen Objekte (AG-Replika, Publikationen, ...) bereits
    separat behandelt wurden oder bewusst in Kauf genommen werden.
 
.PARAMETER EnableException
    Wirft Exceptions statt sie nur im Ergebnisobjekt zu melden.
 
.OUTPUTS
    [PSCustomObject] mit:
        SqlInstance : Instanzname (wie uebergeben)
        RegisteredServerName : @@SERVERNAME vor der Aenderung
        ExpectedServerName : Aus MachineName/InstanceName berechneter Soll-Name
        IsClustered : SERVERPROPERTY('IsClustered') = 1?
        Status : AlreadyCorrect | Changed | Blocked | WhatIf | Error
        RestartRequired : $true, wenn ein Dienst-Neustart fuer @@SERVERNAME noch aussteht
        Message : Detailmeldung
 
.EXAMPLE
    Repair-sqmServerName
 
    Prueft die lokale Default-Instanz und korrigiert @@SERVERNAME bei Bedarf (mit Guard-Checks).
 
.EXAMPLE
    Repair-sqmServerName -SqlInstance 'SQL01\INST01' -WhatIf
 
    Zeigt nur an, welche sp_dropserver/sp_addserver-Schritte ausgefuehrt wuerden.
 
.EXAMPLE
    Repair-sqmServerName -SqlInstance 'SQL01' -Force -Confirm:$false
 
    Erzwingt die Umbenennung auch wenn AG-/Replikations-/Cluster-Referenzen erkannt wurden.
 
.NOTES
    Nach der Aenderung ist ein Neustart des SQL Server-Dienstes erforderlich, damit @@SERVERNAME
    den neuen Wert liefert. sys.servers wird sofort aktualisiert, die Session-Funktion
    @@SERVERNAME/SERVERPROPERTY('ServerName') jedoch erst nach dem Neustart.
    Bei Failover-Cluster-Instanzen (FCI) und AlwaysOn wird der Netzwerkname ueber die
    Cluster-/AG-Konfiguration verwaltet - dort ist i. d. R. keine manuelle Korrektur noetig bzw.
    diese Funktion ist der falsche Weg (siehe Guard-Check IsClustered).
#>

function Repair-sqmServerName
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string]$SqlInstance = $env:COMPUTERNAME,
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$SqlCredential,
        [Parameter(Mandatory = $false)]
        [switch]$Force,
        [Parameter(Mandatory = $false)]
        [switch]$EnableException
    )

    $functionName = $MyInvocation.MyCommand.Name

    function _Log { param([string]$Msg, [string]$Level = 'INFO')
        Write-Verbose "[$functionName] $Msg"
        try { Invoke-sqmLogging -Message $Msg -FunctionName $functionName -Level $Level } catch { }
    }

    $result = [PSCustomObject]@{
        SqlInstance             = $SqlInstance
        RegisteredServerName = $null
        ExpectedServerName   = $null
        IsClustered             = $null
        Status                 = 'Error'
        RestartRequired         = $false
        Message                 = $null
    }

    try
    {
        if (-not (Get-Module -ListAvailable -Name dbatools))
        {
            throw "dbatools-Modul nicht gefunden."
        }

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

        _Log "Starte $functionName auf $SqlInstance."
        Connect-DbaInstance @connParams -ErrorAction Stop | Out-Null

        # --- Registrierten vs. erwarteten Servernamen ermitteln ---
        $props = Invoke-DbaQuery @connParams -Query @"
SELECT
    CAST(SERVERPROPERTY('ServerName') AS sysname) AS RegisteredServerName,
    CAST(SERVERPROPERTY('MachineName') AS sysname) AS MachineName,
    CAST(SERVERPROPERTY('InstanceName') AS sysname) AS InstanceName,
    CAST(SERVERPROPERTY('IsClustered') AS int) AS IsClustered
"@
 -ErrorAction Stop

        $registeredName = $props.RegisteredServerName
        $expectedName = if ([string]::IsNullOrEmpty($props.InstanceName)) { $props.MachineName }
        else { "$($props.MachineName)\$($props.InstanceName)" }

        $result.RegisteredServerName = $registeredName
        $result.ExpectedServerName = $expectedName
        $result.IsClustered = [bool]$props.IsClustered

        _Log "Registriert: '$registeredName' | Erwartet (MachineName/InstanceName): '$expectedName' | IsClustered: $($result.IsClustered)"

        if ($registeredName -eq $expectedName)
        {
            $result.Status = 'AlreadyCorrect'
            $result.Message = "@@SERVERNAME ('$registeredName') stimmt bereits mit dem Windows-Namen ueberein - keine Aenderung noetig."
            _Log $result.Message 'INFO'
            Write-Host " OK: $($result.Message)" -ForegroundColor Green
            return $result
        }

        # --- Guard-Checks (ueberspringbar mit -Force) ---
        if (-not $Force)
        {
            if ($result.IsClustered)
            {
                $result.Status = 'Blocked'
                $result.Message = "Instanz ist Teil eines Failover-Clusters (IsClustered=1). Der Netzwerkname wird " +
                "ueber die Cluster-Konfiguration verwaltet - sp_dropserver/sp_addserver ist hier i. d. R. der " +
                "falsche Weg. Mit -Force erzwingen, falls die Umbenennung dennoch beabsichtigt ist."
                _Log $result.Message 'WARNING'
                Write-Warning $result.Message
                return $result
            }

            $agCount = Invoke-DbaQuery @connParams -Query "SELECT COUNT(*) AS Cnt FROM sys.availability_replicas WHERE replica_server_name = @@SERVERNAME" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Cnt
            if ($agCount -gt 0)
            {
                $agNames = Invoke-DbaQuery @connParams -Query "SELECT ag.name FROM sys.availability_groups ag JOIN sys.availability_replicas ar ON ag.group_id = ar.group_id WHERE ar.replica_server_name = @@SERVERNAME" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty name
                $result.Status = 'Blocked'
                $result.Message = "AlwaysOn Availability Group erkannt ($($agNames -join ', ')). replica_server_name " +
                "referenziert den alten Namen - eine Umbenennung erfordert das Entfernen/Neuanlegen des Replikas " +
                "in der AG. Mit -Force erzwingen, wenn das bereits separat behandelt wurde."
                _Log $result.Message 'WARNING'
                Write-Warning $result.Message
                return $result
            }

            $replRoles = Invoke-DbaQuery @connParams -Query "SELECT name FROM sys.databases WHERE is_distributor = 1 OR is_published = 1 OR is_subscribed = 1 OR is_merge_published = 1" -ErrorAction SilentlyContinue
            if ($replRoles)
            {
                $result.Status = 'Blocked'
                $result.Message = "Replikationsrolle erkannt (Distributor/Publisher/Subscriber bei: $(($replRoles | Select-Object -ExpandProperty name) -join ', ')). " +
                "Publikationen/Subscriptions referenzieren den alten Servernamen. Mit -Force erzwingen, wenn " +
                "die Replikation bereits separat behandelt wurde."
                _Log $result.Message 'WARNING'
                Write-Warning $result.Message
                return $result
            }
        }

        $repairAction = "sp_dropserver '$registeredName' + sp_addserver '$expectedName', local"
        if ($PSCmdlet.ShouldProcess($SqlInstance, $repairAction))
        {
            $oldEsc = $registeredName -replace "'", "''"
            $newEsc = $expectedName -replace "'", "''"

            _Log "Fuehre sp_dropserver '$registeredName' aus." 'INFO'
            Invoke-DbaQuery @connParams -Database 'master' -Query "EXEC sp_dropserver @server = N'$oldEsc';" -ErrorAction Stop | Out-Null

            try
            {
                _Log "Fuehre sp_addserver '$expectedName' (local) aus." 'INFO'
                Invoke-DbaQuery @connParams -Database 'master' -Query "EXEC sp_addserver @server = N'$newEsc', local;" -ErrorAction Stop | Out-Null
            }
            catch
            {
                # Kritisch: sp_dropserver ist bereits durch - die Instanz hat jetzt KEINEN lokalen
                # Servereintrag mehr. Klar melden statt still zu verschlucken.
                $result.Status = 'Error'
                $result.Message = "sp_dropserver('$registeredName') war erfolgreich, sp_addserver('$expectedName') " +
                "ist fehlgeschlagen: $($_.Exception.Message). Die Instanz hat aktuell KEINEN lokalen Server-Eintrag " +
                "mehr - manuell mit 'EXEC sp_addserver N''$newEsc'', local;' nachziehen."
                _Log $result.Message 'ERROR'
                Write-Error $result.Message -ErrorAction Continue
                if ($EnableException) { throw }
                return $result
            }

            $result.Status = 'Changed'
            $result.RestartRequired = $true
            $result.Message = "Servername von '$registeredName' auf '$expectedName' korrigiert. " +
            "Neustart des SQL Server-Dienstes erforderlich, bis @@SERVERNAME den neuen Wert zurueckgibt."
            _Log $result.Message 'INFO'
            Write-Host " OK: $($result.Message)" -ForegroundColor Green
        }
        else
        {
            $result.Status = 'WhatIf'
            $result.Message = "WhatIf: '$registeredName' -> '$expectedName' ($repairAction), keine Aenderung vorgenommen."
            _Log $result.Message 'INFO'
            Write-Host " [WhatIf] $($result.Message)" -ForegroundColor Yellow
        }
    }
    catch
    {
        $result.Status = 'Error'
        $result.Message = "Fehler in $functionName : $($_.Exception.Message)"
        _Log $result.Message 'ERROR'
        Write-Error $result.Message -ErrorAction Continue
        if ($EnableException) { throw }
    }

    return $result
}