bin/Public/Invoke-sqmRestoreDatabase.ps1
|
<#
.SYNOPSIS Stellt eine Datenbank aus einer Backup-Datei wieder her, mit Unterstuetzung fuer Single-Server und AlwaysOn. .DESCRIPTION Die Funktion fuehrt einen kontrollierten Restore einer Datenbank durch. Sie erkennt automatisch, ob die Zieldatenbank zu einer AlwaysOn-Verfuegbarkeitsgruppe gehoert, und entfernt sie in diesem Fall aus der AG (inkl. Loeschung auf sekundaeren Replikaten). Vor dem Restore werden die Datenbank-User exportiert (fuer spaetere Wiederherstellung). Optional kann ein Backup der urspruenglichen Datenbank erstellt werden. Nach dem Restore werden die User wiederhergestellt, verwaiste User repariert, nicht mehr existierende Windows-Logins entfernt und der Datenbank-Eigentuemer auf das sa-Konto (unabhaengig vom Namen) gesetzt. Die Funktion kann auch die Wiederherstellung einer Sequenz von Backups (Full + Diff + Logs) durchfuehren. Dazu wird der Parameter `-BackupFiles` verwendet, der eine Liste von Backup-Dateien in der richtigen Reihenfolge (Full, dann Diff, dann Logs) akzeptiert. Vor dem Export der User und vor dem Restore wird die konfigurierte PBM-Policy (DefaultPolicy) temporaer deaktiviert, um Einschraenkungen bei der User-Erstellung zu vermeiden. Nach Abschluss wird sie wieder aktiviert. Falls die Datenbank vor dem Restore in Benutzung ist, wird sie automatisch in den Single-User-Modus versetzt (und nach dem Restore wieder in Multi-User zurueckgesetzt). .PARAMETER SqlInstance Ziel-SQL Server-Instanz (z.B. "localhost", "SQL01\INSTANCE"). Default: aktueller Computername. .PARAMETER SqlCredential Alternative Anmeldeinformationen fuer die Zielinstanz. .PARAMETER BackupFile Pfad zur Full-Backup-Datei (.bak). Kann auch ein Array sein, wenn mehrere Dateien (z.B. Stripes). Fuer sequenzielle Wiederherstellung (Full + Diff + Logs) verwenden Sie `-BackupFiles`. .PARAMETER BackupFiles Array von Backup-Dateien in der Reihenfolge: Full, dann Diff (optional), dann Logs (optional). Beispiel: @("C:\Backup\Full.bak", "C:\Backup\Diff.bak", "C:\Backup\Log1.trn", "C:\Backup\Log2.trn"). Kann anstelle von `-BackupFile` verwendet werden. .PARAMETER DatabaseName Name der wiederherzustellenden Datenbank (wie sie in der Backup-Datei heisst). Wird benoetigt, um die Dateinamen zu ermitteln. .PARAMETER NewDatabaseName Optional: Neuer Name fuer die Datenbank nach dem Restore. Wenn angegeben, werden die logischen Dateinamen entsprechend angepasst (die physikalischen Dateien erhalten den neuen Namen als Basis). .PARAMETER NewDatabaseFilePath Optional: Zielverzeichnis fuer die Datenbank-Dateien (.mdf, .ndf). Wenn nicht angegeben, wird das Standardverzeichnis der Zielinstanz verwendet (BackupDirectory oder DefaultFile). .PARAMETER NewLogFilePath Optional: Zielverzeichnis fuer die Log-Datei (.ldf). Wenn nicht angegeben, wird das Standardverzeichnis der Zielinstanz verwendet. .PARAMETER BackupBeforeRestore Optional: Erstellt vor dem Restore ein Full-Backup der bestehenden Datenbank (falls vorhanden). Das Backup wird im Standard-Backupverzeichnis mit dem Namen "DatabaseName_preRestore_YYYYMMDD_HHmsqm.bak" abgelegt. .PARAMETER NoUserExport Optional: ueberspringt den Export der Datenbank-User (standardmaessig werden User immer exportiert). Die Export-Datei wird temporaer im %TEMP% Verzeichnis abgelegt. .PARAMETER KeepAlwaysOn Optional: Wenn die Datenbank Teil einer AG ist, wird sie nicht aus der AG entfernt. Achtung: Ein Restore auf eine AG-Datenbank ist nur moeglich, wenn die Datenbank vorher aus der AG genommen wird. Daher sollte dieser Parameter nur verwendet werden, wenn die Datenbank bereits nicht in der AG ist. .PARAMETER WithNoRecovery Optional: Fuehrt den Restore mit NORECOVERY aus, so dass die Datenbank im wiederherstellenden Zustand bleibt (fuer weitere Log-Backups). Standardmaessig wird RECOVERY verwendet (Datenbank online). .PARAMETER ContinueWithNoRecovery Optional: Wenn gesetzt, wird auch der letzte Restore mit NORECOVERY ausgefuehrt (z.B. wenn manuell weitere Backups angewendet werden sollen). .PARAMETER ForceSingleUser Erzwingt, dass die Datenbank vor dem Restore in den Single-User-Modus versetzt wird (auch wenn sie nicht in Benutzung scheint). Standardmaessig wird nur bei aktiven Verbindungen umgeschaltet. .PARAMETER RejoinAvailabilityGroup Wenn gesetzt und die Datenbank war Teil einer AG, wird sie nach dem Restore automatisch wieder in die AG aufgenommen (Add-DbaAgDatabase mit SeedingMode Automatic). Voraussetzung: Automatic Seeding ist auf der AG konfiguriert. Ohne diesen Parameter bleibt die Datenbank nach dem Restore ausserhalb der AG. .PARAMETER EnableException Schalter, um Ausnahmen durchzulassen (standardmaessig werden Fehler protokolliert und als Objekt zurueckgegeben). .PARAMETER Confirm Fordert vor kritischen Aktionen (Loeschen der Datenbank aus AG, Restore) eine Bestaetigung an. .PARAMETER WhatIf Zeigt, was passieren wuerde, ohne aenderungen durchzufuehren. .EXAMPLE # Einfacher Restore einer Full-Backup-Datei Invoke-sqmRestoreDatabase -SqlInstance "SQL01" -BackupFile "D:\Backup\AdventureWorks.bak" -DatabaseName "AdventureWorks" .EXAMPLE # Restore mit Full + Diff + Logs $backupSequence = @( "D:\Backup\AdventureWorks_Full.bak", "D:\Backup\AdventureWorks_Diff.bak", "D:\Backup\AdventureWorks_Log1.trn", "D:\Backup\AdventureWorks_Log2.trn" ) Invoke-sqmRestoreDatabase -SqlInstance "SQL01" -BackupFiles $backupSequence -DatabaseName "AdventureWorks" .EXAMPLE # Restore mit neuem Namen und Single-User-Erzwingung Invoke-sqmRestoreDatabase -SqlInstance "SQL01" -BackupFile "D:\Backup\OldDB.bak" -DatabaseName "OldDB" -NewDatabaseName "NewDB" -ForceSingleUser .NOTES Erfordert dbatools-Modul, Invoke-sqmLogging, Get-sqmConfig, Set-sqmSqlPolicyState. Die Funktion setzt voraus, dass der ausfuehrende Login sysadmin-Rechte auf der Zielinstanz und allen sekundaeren Replikaten hat. #> function Invoke-sqmRestoreDatabase { [CmdletBinding(DefaultParameterSetName = 'SingleFile', SupportsShouldProcess = $true, ConfirmImpact = 'None')] param ( [Parameter(Mandatory = $false, Position = 0)] [string]$SqlInstance, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$SqlCredential, [Parameter(Mandatory = $true, ParameterSetName = 'SingleFile')] [ValidateScript({ Test-Path $_ -PathType Leaf })] [string[]]$BackupFile, [Parameter(Mandatory = $true, ParameterSetName = 'Sequence')] [string[]]$BackupFiles, [Parameter(Mandatory = $true)] [string]$DatabaseName, [Parameter(Mandatory = $false)] [string]$NewDatabaseName, [Parameter(Mandatory = $false)] [string]$NewDatabaseFilePath, [Parameter(Mandatory = $false)] [string]$NewLogFilePath, [Parameter(Mandatory = $false)] [switch]$BackupBeforeRestore, [Parameter(Mandatory = $false)] [switch]$NoUserExport, [Parameter(Mandatory = $false)] [switch]$KeepAlwaysOn, [Parameter(Mandatory = $false)] [switch]$WithNoRecovery, [Parameter(Mandatory = $false)] [switch]$ContinueWithNoRecovery, [Parameter(Mandatory = $false)] [switch]$ForceSingleUser, [Parameter(Mandatory = $false)] [switch]$RejoinAvailabilityGroup, [Parameter(Mandatory = $false)] [switch]$EnableException ) begin { $functionName = $MyInvocation.MyCommand.Name if (-not $PSBoundParameters.ContainsKey('SqlInstance') -or [string]::IsNullOrWhiteSpace($SqlInstance)) { $SqlInstance = $env:COMPUTERNAME Write-Verbose "Keine SqlInstance angegeben. Verwende Standard: $SqlInstance" } if (-not $script:dbatoolsAvailable) { $errMsg = "dbatools-Modul nicht gefunden. Bitte installieren: Install-Module dbatools" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" throw $errMsg } Invoke-sqmLogging -Message "Starte $functionName auf Instanz: $SqlInstance fuer Datenbank '$DatabaseName'" -FunctionName $functionName -Level "INFO" $results = @() $tempDir = [System.IO.Path]::GetTempPath() $userExportFile = Join-Path $tempDir "UserExport_$DatabaseName_$(Get-Date -Format 'yyyyMMddHHmsqm').sql" $isAGDatabase = $false $availabilityGroup = $null $primaryInstance = $null $secondaryInstances = @() $wasSingleUser = $false $originalDbStatus = $null # Policy-Kontrolle $policyName = Get-sqmConfig -Key 'DefaultPolicy' 3>$null $policyWasEnabled = $false $policyDeactivated = $false # Bestimme die Liste der Backup-Dateien je nach Parametersatz $backupFileList = if ($PSCmdlet.ParameterSetName -eq 'Sequence') { $BackupFiles } else { $BackupFile } } process { try { # ---- Vorbereitung: Policy temporaer deaktivieren ---- if (-not [string]::IsNullOrWhiteSpace($policyName)) { try { $policyObj = Get-DbaPbmPolicy -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Policy $policyName -ErrorAction SilentlyContinue if ($policyObj -and $policyObj.Policy.Enabled) { $policyWasEnabled = $true if ($PSCmdlet.ShouldProcess($SqlInstance, "Temporaer Policy '$policyName' deaktivieren fuer Restore-Operation")) { Invoke-sqmLogging -Message "Deaktiviere Policy '$policyName' temporaer." -FunctionName $functionName -Level "INFO" Set-sqmSqlPolicyState -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Policy $policyName -State Disable -EnableException:$EnableException -Confirm:$false $policyDeactivated = $true $results += [PSCustomObject]@{ Action = "PolicyTemporaryDisable"; Status = "Success"; Message = "Policy '$policyName' deaktiviert." } } else { $results += [PSCustomObject]@{ Action = "PolicyTemporaryDisable"; Status = "Skipped"; Message = "WhatIf - Policy-Deaktivierung uebersprungen." } } } } catch { Invoke-sqmLogging -Message "Warnung: Konnte Policy '$policyName' nicht pruefen: $($_.Exception.Message)" -FunctionName $functionName -Level "WARNING" } } # ---- 1. Zielinstanz und Datenbank-Status ermitteln ---- $server = Connect-DbaInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential -ErrorAction Stop $targetDb = $server.Databases[$DatabaseName] $dbExists = $targetDb -ne $null if ($dbExists) { Invoke-sqmLogging -Message "Datenbank '$DatabaseName' existiert auf $SqlInstance." -FunctionName $functionName -Level "INFO" # Pruefen, ob die Datenbank in einer AG ist $agCheck = Get-DbaAvailabilityGroup -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $DatabaseName -ErrorAction SilentlyContinue if ($agCheck) { $isAGDatabase = $true $availabilityGroup = $agCheck Invoke-sqmLogging -Message "Datenbank ist Mitglied der AG '$($availabilityGroup.Name)'." -FunctionName $functionName -Level "INFO" if (-not $KeepAlwaysOn) { Invoke-sqmLogging -Message "Die Datenbank wird aus der AG entfernt (einschliesslich sekundaerer Replikate)." -FunctionName $functionName -Level "INFO" } else { Invoke-sqmLogging -Message "KeepAlwaysOn ist gesetzt - die Datenbank verbleibt in der AG. Ein Restore ist in einer AG nicht moeglich, daher wird der Vorgang abgebrochen." -FunctionName $functionName -Level "ERROR" throw "Datenbank ist Teil einer AG und KeepAlwaysOn wurde angegeben. Restore nicht moeglich." } } # ---- Datenbank in Single-User-Modus versetzen, falls noetig ---- $activeConnections = $targetDb.ActiveConnections if ($activeConnections -gt 0 -or $ForceSingleUser) { if ($activeConnections -gt 0) { Invoke-sqmLogging -Message "Datenbank '$DatabaseName' hat $activeConnections aktive Verbindungen. Setze in Single-User-Modus." -FunctionName $functionName -Level "INFO" } else { Invoke-sqmLogging -Message "Erzwinge Single-User-Modus fuer Datenbank '$DatabaseName'." -FunctionName $functionName -Level "INFO" } $setSingleUserAction = "Setze Datenbank '$DatabaseName' in Single-User-Modus" if ($PSCmdlet.ShouldProcess($DatabaseName, $setSingleUserAction)) { try { $singleUserQuery = "ALTER DATABASE [$DatabaseName] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;" Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database master -Query $singleUserQuery -ErrorAction Stop $wasSingleUser = $true Invoke-sqmLogging -Message "Datenbank '$DatabaseName' jetzt im Single-User-Modus." -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "SetSingleUser"; Status = "Success"; Message = "Datenbank in Single-User versetzt." } } catch { $errMsg = "Fehler beim Setzen des Single-User-Modus: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results += [PSCustomObject]@{ Action = "SetSingleUser"; Status = "Failed"; Message = $errMsg } return $results } } else { $results += [PSCustomObject]@{ Action = "SetSingleUser"; Status = "Skipped"; Message = "WhatIf - Single-User uebersprungen." } } } else { Invoke-sqmLogging -Message "Datenbank '$DatabaseName' hat keine aktiven Verbindungen." -FunctionName $functionName -Level "INFO" } } else { Invoke-sqmLogging -Message "Datenbank '$DatabaseName' existiert nicht auf $SqlInstance." -FunctionName $functionName -Level "INFO" } # ---- 2. Optional: Backup der vorhandenen Datenbank ---- if ($BackupBeforeRestore -and $dbExists -and -not $isAGDatabase) { $backupFileName = "${DatabaseName}_preRestore_$(Get-Date -Format 'yyyyMMdd_HHmsqm').bak" $backupFileFull = Join-Path (Get-DbaDefaultPath -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Type Backup) $backupFileName $backupParams = @{ SqlInstance = $SqlInstance SqlCredential = $SqlCredential Database = $DatabaseName Path = $backupFileFull Type = 'Full' ErrorAction = 'Stop' } if ($PSCmdlet.ShouldProcess($DatabaseName, "Backup der Datenbank '$DatabaseName' nach $backupFileFull")) { try { Invoke-sqmLogging -Message "Erstelle Backup der vorhandenen Datenbank: $backupFileFull" -FunctionName $functionName -Level "INFO" Backup-DbaDatabase @backupParams Invoke-sqmLogging -Message "Backup erfolgreich." -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "PreRestoreBackup"; Status = "Success"; Message = "Backup erstellt: $backupFileFull" } } catch { $errMsg = "Fehler beim Backup vor dem Restore: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results += [PSCustomObject]@{ Action = "PreRestoreBackup"; Status = "Failed"; Message = $errMsg } return $results } } else { $results += [PSCustomObject]@{ Action = "PreRestoreBackup"; Status = "Skipped"; Message = "WhatIf - Backup uebersprungen." } } } # ---- 3. Export der Datenbank-User (immer, es sei denn NoUserExport) ---- if (-not $NoUserExport -and $dbExists) { if ($PSCmdlet.ShouldProcess($DatabaseName, "Export der Datenbank-User nach $userExportFile")) { try { Invoke-sqmLogging -Message "Exportiere User der Datenbank '$DatabaseName' nach $userExportFile" -FunctionName $functionName -Level "INFO" Export-DbaUser -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $DatabaseName -Path $userExportFile -Force -ErrorAction Stop Invoke-sqmLogging -Message "User-Export erfolgreich." -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "UserExport"; Status = "Success"; Message = "Exportdatei: $userExportFile" } } catch { $errMsg = "Fehler beim Export der User: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results += [PSCustomObject]@{ Action = "UserExport"; Status = "Failed"; Message = $errMsg } return $results } } else { $results += [PSCustomObject]@{ Action = "UserExport"; Status = "Skipped"; Message = "WhatIf - User-Export uebersprungen." } } } # ---- 4. Wenn AlwaysOn: Datenbank aus der AG entfernen (primaer) und von sekundaeren Replikaten loeschen ---- if ($isAGDatabase -and -not $KeepAlwaysOn) { $replicas = Get-DbaAgReplica -SqlInstance $SqlInstance -SqlCredential $SqlCredential -AvailabilityGroup $availabilityGroup.Name -ErrorAction Stop $primaryReplica = $replicas | Where-Object { $_.Role -eq 'Primary' } | Select-Object -First 1 $secondaryReplicas = $replicas | Where-Object { $_.Role -eq 'Secondary' } $secondaryInstances = $secondaryReplicas | Select-Object -ExpandProperty Name if ($primaryReplica.Name -ne $SqlInstance) { Invoke-sqmLogging -Message "Aktuelle Instanz ist nicht primaer. Verbinde mit primaerer Instanz $($primaryReplica.Name) fuer AG-Operationen." -FunctionName $functionName -Level "INFO" $primaryInstance = $primaryReplica.Name } else { $primaryInstance = $SqlInstance } $removeAgAction = "Entferne Datenbank '$DatabaseName' aus der AG '$($availabilityGroup.Name)'" if ($PSCmdlet.ShouldProcess($DatabaseName, $removeAgAction)) { try { Invoke-sqmLogging -Message $removeAgAction -FunctionName $functionName -Level "INFO" Remove-DbaAgDatabase -SqlInstance $primaryInstance -SqlCredential $SqlCredential -AvailabilityGroup $availabilityGroup.Name -Database $DatabaseName -ErrorAction Stop Invoke-sqmLogging -Message "Datenbank erfolgreich aus AG entfernt." -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "RemoveFromAG"; Status = "Success"; Message = "Datenbank aus AG entfernt." } } catch { $errMsg = "Fehler beim Entfernen aus AG: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results += [PSCustomObject]@{ Action = "RemoveFromAG"; Status = "Failed"; Message = $errMsg } return $results } } else { $results += [PSCustomObject]@{ Action = "RemoveFromAG"; Status = "Skipped"; Message = "WhatIf - Entfernen aus AG uebersprungen." } } foreach ($secondary in $secondaryInstances) { $removeDbAction = "Loesche Datenbank '$DatabaseName' auf sekundaerem Knoten '$secondary'" if ($PSCmdlet.ShouldProcess($DatabaseName, $removeDbAction)) { try { Invoke-sqmLogging -Message $removeDbAction -FunctionName $functionName -Level "INFO" $secondaryServer = Connect-DbaInstance -SqlInstance $secondary -SqlCredential $SqlCredential -ErrorAction Stop if ($secondaryServer.Databases[$DatabaseName] -and $secondaryServer.Databases[$DatabaseName].IsAccessible) { Remove-DbaDatabase -SqlInstance $secondary -SqlCredential $SqlCredential -Database $DatabaseName -Confirm:$false -ErrorAction Stop Invoke-sqmLogging -Message "Datenbank auf '$secondary' geloescht." -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "RemoveFromSecondary"; Target = $secondary; Status = "Success"; Message = "Datenbank auf sekundaerem Knoten geloescht." } } else { Invoke-sqmLogging -Message "Datenbank auf '$secondary' nicht vorhanden." -FunctionName $functionName -Level "VERBOSE" } } catch { $errMsg = "Fehler beim Loeschen auf '$secondary': $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results += [PSCustomObject]@{ Action = "RemoveFromSecondary"; Target = $secondary; Status = "Failed"; Message = $errMsg } } } else { $results += [PSCustomObject]@{ Action = "RemoveFromSecondary"; Target = $secondary; Status = "Skipped"; Message = "WhatIf - Loeschen uebersprungen." } } } } # ---- 5. Restore der Datenbank(en) ---- $finalDbName = if ($NewDatabaseName) { $NewDatabaseName } else { $DatabaseName } $restoreCount = 0 $totalFiles = $backupFileList.Count foreach ($file in $backupFileList) { $restoreCount++ $isLast = ($restoreCount -eq $totalFiles) $useRecovery = if ($ContinueWithNoRecovery) { $false } elseif ($WithNoRecovery) { $false } else { $isLast } $restoreParams = @{ SqlInstance = $SqlInstance SqlCredential = $SqlCredential Path = $file DatabaseName = $DatabaseName WithReplace = $true NoRecovery = (-not $useRecovery) ErrorAction = 'Stop' } if ($NewDatabaseName) { $restoreParams.NewDatabaseName = $NewDatabaseName } if ($NewDatabaseFilePath) { $restoreParams.DatabaseFilePath = $NewDatabaseFilePath } if ($NewLogFilePath) { $restoreParams.LogFilePath = $NewLogFilePath } # Fuer alle ausser den ersten Restore (Full) muss der Datenbankname bereits existieren; fuer Log-Restores ist das wichtig # Restore-DbaDatabase kann sequenziell verarbeitet werden. # Auto-FileMapping: beim ersten Restore (Full) immer FileMapping aus RESTORE FILELISTONLY aufbauen. # Das verhindert Pfadkonflikte wenn das Backup bereits vorhandene Dateipfade enthaelt. if ($restoreCount -eq 1) { try { $safeFilePath = $file.Replace("'", "''") $backupFileListing = Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential ` -Database 'master' ` -Query "RESTORE FILELISTONLY FROM DISK = N'$safeFilePath'" ` -ErrorAction Stop if ($backupFileListing) { $defaultPaths = Get-DbaDefaultPath -SqlInstance $SqlInstance -SqlCredential $SqlCredential $dataDir = if ($NewDatabaseFilePath) { $NewDatabaseFilePath } else { $defaultPaths.Data } $logDir = if ($NewLogFilePath) { $NewLogFilePath } else { $defaultPaths.Log } $fileMapping = @{} $logIdx = 0 $datIdx = 0 foreach ($backupFileEntry in $backupFileListing) { $ext = [System.IO.Path]::GetExtension($backupFileEntry.PhysicalName) $isLog = ($backupFileEntry.Type -eq 'L') $dir = if ($isLog) { $logDir } else { $dataDir } if ($isLog) { $logIdx++ $sfx = if ($logIdx -eq 1) { '_log' } else { "_log$logIdx" } } else { $datIdx++ $sfx = if ($datIdx -eq 1) { '' } else { "_$datIdx" } } $fileMapping[$backupFileEntry.LogicalName] = Join-Path $dir "$finalDbName$sfx$ext" Invoke-sqmLogging -Message "FileMapping: '$($backupFileEntry.LogicalName)' -> '$($fileMapping[$backupFileEntry.LogicalName])'" ` -FunctionName $functionName -Level "INFO" } $restoreParams['FileMapping'] = $fileMapping } } catch { Invoke-sqmLogging -Message "Auto-FileMapping fehlgeschlagen (wird uebersprungen): $($_.Exception.Message)" ` -FunctionName $functionName -Level "WARNING" } } $restoreAction = "Restore von $file ($restoreCount/$totalFiles) fuer Datenbank '$DatabaseName'" if ($NewDatabaseName) { $restoreAction += " als '$NewDatabaseName'" } if (-not $useRecovery) { $restoreAction += " (NORECOVERY)" } if ($PSCmdlet.ShouldProcess($DatabaseName, $restoreAction)) { try { Invoke-sqmLogging -Message $restoreAction -FunctionName $functionName -Level "INFO" $restoreResult = Restore-DbaDatabase @restoreParams Invoke-sqmLogging -Message "Restore von $file erfolgreich." -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "RestoreStep"; File = $file; Step = $restoreCount; Status = "Success"; Message = "Wiederhergestellt." } } catch { $errMsg = "Fehler beim Restore von $file : $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results += [PSCustomObject]@{ Action = "RestoreStep"; File = $file; Step = $restoreCount; Status = "Failed"; Message = $errMsg } return $results } } else { $results += [PSCustomObject]@{ Action = "RestoreStep"; File = $file; Step = $restoreCount; Status = "Skipped"; Message = "WhatIf - Restore uebersprungen." } return $results } } # ---- 6. Nach dem Restore: User wiederherstellen (wenn Export durchgefuehrt) ---- if (-not $NoUserExport -and (Test-Path $userExportFile)) { $importAction = "Importiere User aus $userExportFile in Datenbank '$finalDbName'" if ($PSCmdlet.ShouldProcess($finalDbName, $importAction)) { try { Invoke-sqmLogging -Message $importAction -FunctionName $functionName -Level "INFO" $sql = Get-Content $userExportFile -Raw Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $finalDbName -Query $sql -ErrorAction Stop Invoke-sqmLogging -Message "User-Import erfolgreich." -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "UserImport"; Status = "Success"; Message = "User aus Export wiederhergestellt." } } catch { $errMsg = "Fehler beim Import der User: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results += [PSCustomObject]@{ Action = "UserImport"; Status = "Failed"; Message = $errMsg } } } else { $results += [PSCustomObject]@{ Action = "UserImport"; Status = "Skipped"; Message = "WhatIf - User-Import uebersprungen." } } } # ---- 7. Verwaiste User reparieren ---- $orphanFixAction = "Repariere verwaiste User in Datenbank '$finalDbName'" if ($PSCmdlet.ShouldProcess($finalDbName, $orphanFixAction)) { try { Invoke-sqmLogging -Message $orphanFixAction -FunctionName $functionName -Level "INFO" $repairResult = Repair-DbaDbOrphanUser -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $finalDbName -ErrorAction Stop $repairedCount = if ($repairResult) { @($repairResult).Count } else { 0 } Invoke-sqmLogging -Message "Verwaiste User repariert: $repairedCount." -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "FixOrphans"; Status = "Success"; Message = "Repair-DbaDbOrphanUser: $repairedCount User repariert." } } catch { $errMsg = "Fehler bei der Reparatur verwaister User: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results += [PSCustomObject]@{ Action = "FixOrphans"; Status = "Failed"; Message = $errMsg } } } else { $results += [PSCustomObject]@{ Action = "FixOrphans"; Status = "Skipped"; Message = "WhatIf - Reparatur uebersprungen." } } # ---- 8. Domaenenfremde Accounts entfernen ---- $removeOrphanLoginsAction = "Entferne nicht mehr existierende Windows-Logins aus Datenbank '$finalDbName'" if ($PSCmdlet.ShouldProcess($finalDbName, $removeOrphanLoginsAction)) { try { Invoke-sqmLogging -Message $removeOrphanLoginsAction -FunctionName $functionName -Level "INFO" $query = @" DECLARE @dbname sysname = DB_NAME(); SELECT dp.name AS UserName FROM sys.database_principals dp LEFT JOIN sys.server_principals sp ON dp.sid = sp.sid WHERE dp.type IN ('U', 'G') AND sp.sid IS NULL AND dp.name NOT IN ('dbo', 'guest', 'INFORMATION_SCHEMA', 'sys') "@ $missingLogins = Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $finalDbName -Query $query -ErrorAction Stop foreach ($login in $missingLogins) { $userName = $login.UserName Invoke-sqmLogging -Message "Entferne Windows-User '$userName' (Login existiert nicht mehr)." -FunctionName $functionName -Level "DEBUG" $dropQuery = "DROP USER [$userName]" Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $finalDbName -Query $dropQuery -ErrorAction SilentlyContinue } Invoke-sqmLogging -Message "Nicht mehr existierende Windows-Logins wurden entfernt." -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "RemoveOrphanWindowsLogins"; Status = "Success"; Message = "Entfernt: $($missingLogins.Count) User." } } catch { $errMsg = "Fehler beim Entfernen nicht existierender Windows-Logins: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results += [PSCustomObject]@{ Action = "RemoveOrphanWindowsLogins"; Status = "Failed"; Message = $errMsg } } } else { $results += [PSCustomObject]@{ Action = "RemoveOrphanWindowsLogins"; Status = "Skipped"; Message = "WhatIf - Entfernen uebersprungen." } } # ---- 9. 'sa' Konto als Datenbankeigentuemer setzen ---- $setOwnerAction = "Setze sa-Konto (SID 0x01) als Datenbankeigentuemer fuer '$finalDbName'" if ($PSCmdlet.ShouldProcess($finalDbName, $setOwnerAction)) { try { Invoke-sqmLogging -Message $setOwnerAction -FunctionName $functionName -Level "INFO" $saNameRow = Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential ` -Database 'master' ` -Query "SELECT name FROM sys.server_principals WHERE sid = 0x01" ` -ErrorAction Stop if (-not $saNameRow -or [string]::IsNullOrWhiteSpace($saNameRow.name)) { throw "sa-Login (SID 0x01) nicht gefunden." } $saName = $saNameRow.name Set-DbaDbOwner -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $finalDbName -TargetLogin $saName -ErrorAction Stop Invoke-sqmLogging -Message "Datenbankeigentuemer auf '$saName' gesetzt." -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "SetDbOwner"; Status = "Success"; Message = "Eigentuemer: $saName" } } catch { $errMsg = "Fehler beim Setzen des Datenbankeigentuemers: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results += [PSCustomObject]@{ Action = "SetDbOwner"; Status = "Failed"; Message = $errMsg } } } else { $results += [PSCustomObject]@{ Action = "SetDbOwner"; Status = "Skipped"; Message = "WhatIf - Setzen des Eigentuemers uebersprungen." } } # ---- 10. Optional: Datenbank wieder in die AG aufnehmen ---- if ($RejoinAvailabilityGroup -and $isAGDatabase -and -not $KeepAlwaysOn) { $rejoinAction = "Fuge Datenbank '$finalDbName' wieder in AG '$($availabilityGroup.Name)' ein" if ($PSCmdlet.ShouldProcess($finalDbName, $rejoinAction)) { try { Invoke-sqmLogging -Message $rejoinAction -FunctionName $functionName -Level "INFO" # Pruefe SeedingMode aller Sekundaer-Replikate - stelle Automatic Seeding sicher $agReplicas = Get-DbaAgReplica -SqlInstance $primaryInstance -SqlCredential $SqlCredential ` -AvailabilityGroup $availabilityGroup.Name -ErrorAction Stop foreach ($replica in ($agReplicas | Where-Object { $_.Role -eq 'Secondary' })) { if ($replica.SeedingMode -ne 'Automatic') { Invoke-sqmLogging -Message "Replikat '$($replica.Name)': SeedingMode ist '$($replica.SeedingMode)' - stelle auf Automatic um." ` -FunctionName $functionName -Level "INFO" # Primary-Seite: Replikat auf Automatic Seeding umstellen Set-DbaAgReplica -SqlInstance $primaryInstance -SqlCredential $SqlCredential ` -AvailabilityGroup $availabilityGroup.Name ` -Replica $replica.Name ` -SeedingMode Automatic -ErrorAction Stop # Secondary-Seite: GRANT CREATE ANY DATABASE Invoke-DbaQuery -SqlInstance $replica.Name -SqlCredential $SqlCredential ` -Database master ` -Query "ALTER AVAILABILITY GROUP [$($availabilityGroup.Name)] GRANT CREATE ANY DATABASE" ` -ErrorAction SilentlyContinue Invoke-sqmLogging -Message "Replikat '$($replica.Name)' auf Automatic Seeding umgestellt." ` -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "SetAutoSeeding"; Target = $replica.Name; Status = "Success"; Message = "SeedingMode auf Automatic gesetzt." } } else { Invoke-sqmLogging -Message "Replikat '$($replica.Name)': SeedingMode ist bereits Automatic." ` -FunctionName $functionName -Level "INFO" } } # Datenbank zur AG hinzufuegen Add-DbaAgDatabase -SqlInstance $primaryInstance -SqlCredential $SqlCredential ` -AvailabilityGroup $availabilityGroup.Name ` -Database $finalDbName ` -SeedingMode Automatic ` -ErrorAction Stop Invoke-sqmLogging -Message "Datenbank '$finalDbName' erfolgreich in AG '$($availabilityGroup.Name)' aufgenommen." ` -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "RejoinAG"; Status = "Success"; Message = "Datenbank in AG '$($availabilityGroup.Name)' aufgenommen (Automatic Seeding)." } } catch { $errMsg = "Fehler beim Wiedereinfuegen in die AG: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results += [PSCustomObject]@{ Action = "RejoinAG"; Status = "Failed"; Message = $errMsg } } } else { $results += [PSCustomObject]@{ Action = "RejoinAG"; Status = "Skipped"; Message = "WhatIf - AG-Wiedereinfuegen uebersprungen." } } } # Aufraeumen: temporaere Exportdatei loeschen if (Test-Path $userExportFile) { Remove-Item $userExportFile -Force -ErrorAction SilentlyContinue } } catch { $errMsg = "Allgemeiner Fehler: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results += [PSCustomObject]@{ Action = "GlobalError"; Status = "Failed"; Message = $errMsg } } finally { # ---- Datenbank aus Single-User-Modus zuruecknehmen ---- if ($wasSingleUser) { $setMultiUserAction = "Setze Datenbank '$finalDbName' zurueck in Multi-User-Modus" if ($PSCmdlet.ShouldProcess($finalDbName, $setMultiUserAction)) { try { $multiUserQuery = "ALTER DATABASE [$finalDbName] SET MULTI_USER;" Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database master -Query $multiUserQuery -ErrorAction Stop Invoke-sqmLogging -Message "Datenbank '$finalDbName' wieder im Multi-User-Modus." -FunctionName $functionName -Level "INFO" $results += [PSCustomObject]@{ Action = "SetMultiUser"; Status = "Success"; Message = "Datenbank wieder im Multi-User-Modus." } } catch { Invoke-sqmLogging -Message "Fehler beim Zuruecksetzen des Multi-User-Modus: $($_.Exception.Message)" -FunctionName $functionName -Level "ERROR" $results += [PSCustomObject]@{ Action = "SetMultiUser"; Status = "Failed"; Message = $_.Exception.Message } } } else { $results += [PSCustomObject]@{ Action = "SetMultiUser"; Status = "Skipped"; Message = "WhatIf - Multi-User uebersprungen." } } } # ---- Policy wiederherstellen ---- if ($policyWasEnabled -and $policyDeactivated) { if ($PSCmdlet.ShouldProcess($SqlInstance, "Policy '$policyName' wieder aktivieren")) { Invoke-sqmLogging -Message "Aktiviere Policy '$policyName' wieder." -FunctionName $functionName -Level "INFO" Set-sqmSqlPolicyState -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Policy $policyName -State Enable -EnableException:$EnableException -Confirm:$false $results += [PSCustomObject]@{ Action = "PolicyReenable"; Status = "Success"; Message = "Policy '$policyName' wieder aktiviert." } } else { $results += [PSCustomObject]@{ Action = "PolicyReenable"; Status = "Skipped"; Message = "WhatIf - Policy-Reaktivierung uebersprungen." } } } } } end { Invoke-sqmLogging -Message "$functionName abgeschlossen. $($results.Count) Aktionen protokolliert." -FunctionName $functionName -Level "INFO" return $results } } |