bin/Public/Invoke-sqmTsmConfiguration.ps1
|
<#
.SYNOPSIS Konfiguriert die IBM Spectrum Protect (TSM) Client-Optionsdatei dsm.opt fuer den Einsatz mit SQL Server-Backup-Verzeichnissen. .DESCRIPTION Liest die vorhandene dsm.opt, ergaenzt oder ersetzt die relevanten Eintraege und schreibt die Datei zurueck. Vor jeder aenderung wird automatisch eine Sicherungskopie (dsm.opt.bak) angelegt. Konfigurierte Bereiche: - EXCLUDE fuer SQL Server-Datenbankdateien (*.mdf, *.ndf, *.ldf) - INCLUDE fuer Backup-Verzeichnisse (User-db, Sys-db, zusaetzliche Pfade) - MANAGEMENTCLASS fuer die Backup-Dateien (Aufbewahrungsdauer) Bei Verwendung von -UseDiff wird die Management-Klasse auf MC_B_NL.NL_42.42.NA erzwungen (42 Tage Aufbewahrung). Der Verwaltungsblock in der dsm.opt wird durch die Marker '* --- dtcSqlTools BEGIN ---' und '* --- dtcSqlTools END ---' begrenzt. Manuelle Eintraege ausserhalb bleiben erhalten. .PARAMETER ComputerName Zielrechner (TSM-Client). Standard: aktueller Computername. .PARAMETER SqlInstance SQL Server-Instanz zur Ermittlung des Backup-Verzeichnisses. Standard: $ComputerName. .PARAMETER DsmOptPath Vollstaendiger Pfad zur dsm.opt auf dem Zielrechner. Wird automatisch ermittelt, wenn nicht angegeben. .PARAMETER BackupDirectory Basis-Backup-Verzeichnis. Es werden die Unterverzeichnisse \User-db und \Sys-db als INCLUDE eingetragen. Standard: wird aus der SQL-Instanz ausgelesen (BackupDirectory). .PARAMETER AdditionalIncludePaths Weitere Verzeichnisse, die als INCLUDE eingetragen werden sollen. .PARAMETER ManagementClass TSM Management-Klasse fuer die Backup-Dateien. Erlaubt: MC_B_NL.NL_10.10.NA, MC_B_NL.NL_35.35.NA, MC_B_NL.NL_42.42.NA, MC_B_NL.NL_62.62.NA, MC_B_NL.NL_96.96.NA, MC_B_NL.NL_370.370.NA. Standard: MC_B_NL.NL_42.42.NA. .PARAMETER UseDiff Wenn gesetzt, wird die Management-Klasse auf MC_B_NL.NL_42.42.NA erzwungen (Pflicht fuer Diff-Backup-Strategie). .PARAMETER SqlCredential PSCredential fuer die SQL-Verbindung (zum Auslesen des Backup-Verzeichnisses). .PARAMETER Credential PSCredential fuer Remote-Dateizugriff (Copy-Item, Test-Path) auf dem Zielrechner. .PARAMETER OutputPath Ausgabeverzeichnis fuer den Konfigurationsbericht. Standard: Get-sqmDefaultOutputPath. .PARAMETER ContinueOnError Bei Fehler mit naechstem Schritt fortfahren (hier nicht zutreffend, da keine Schleife). .PARAMETER EnableException Ausnahmen sofort ausloesen (statt stiller Fehlerobjekte). .PARAMETER Confirm Bestaetigung vor dem Schreiben der dsm.opt anfordern. .PARAMETER WhatIf Nur anzeigen, was passieren wuerde. .EXAMPLE Invoke-sqmTsmConfiguration -ManagementClass MC_B_NL.NL_42.42.NA .EXAMPLE Invoke-sqmTsmConfiguration -ComputerName "SQL01" -UseDiff .EXAMPLE Invoke-sqmTsmConfiguration -ComputerName "SQL01" -AdditionalIncludePaths "E:\Archive" .OUTPUTS PSCustomObject mit ComputerName, DsmOptPath, BackupDirectory, ManagementClass, UseDiff, ExcludesWritten, IncludesWritten, BackupCreated, Status, Message, ReportPath. #> function Invoke-sqmTsmConfiguration { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'None')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $false)] [string]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [string]$SqlInstance, [Parameter(Mandatory = $false)] [string]$DsmOptPath, [Parameter(Mandatory = $false)] [string]$BackupDirectory, [Parameter(Mandatory = $false)] [string[]]$AdditionalIncludePaths = @(), [Parameter(Mandatory = $false)] [ValidateSet( 'MC_B_NL.NL_10.10.NA', 'MC_B_NL.NL_35.35.NA', 'MC_B_NL.NL_42.42.NA', 'MC_B_NL.NL_62.62.NA', 'MC_B_NL.NL_96.96.NA', 'MC_B_NL.NL_370.370.NA' )] [string]$ManagementClass = 'MC_B_NL.NL_42.42.NA', [Parameter(Mandatory = $false)] [switch]$UseDiff, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$SqlCredential, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$Credential, [Parameter(Mandatory = $false)] [string]$OutputPath = (Get-sqmDefaultOutputPath), [Parameter(Mandatory = $false)] [switch]$ContinueOnError, [Parameter(Mandatory = $false)] [switch]$EnableException ) begin { $functionName = $MyInvocation.MyCommand.Name if (-not (Get-Module -ListAvailable -Name dbatools)) { $errMsg = "dbatools-Modul nicht gefunden." Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" throw $errMsg } if (-not $SqlInstance) { $SqlInstance = $ComputerName } Invoke-sqmLogging -Message "Starte $functionName auf $ComputerName (SQL-Instanz: $SqlInstance)" -FunctionName $functionName -Level "INFO" } process { $result = [PSCustomObject]@{ ComputerName = $ComputerName DsmOptPath = $null BackupDirectory = $null ManagementClass = $null UseDiff = [bool]$UseDiff ExcludesWritten = 0 IncludesWritten = 0 BackupCreated = $false Status = 'Unknown' Message = $null ReportPath = $null } try { # --- Diff-Validierung --- if ($UseDiff) { if ($ManagementClass -and $ManagementClass -ne 'MC_B_NL.NL_42.42.NA') { $msg = "Bei -UseDiff ist MC_B_NL.NL_42.42.NA Pflicht. Angegebene Klasse '$ManagementClass' ist nicht zulaessig." Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw $msg } $result.Status = 'ValidationFailed' $result.Message = $msg return $result } $ManagementClass = 'MC_B_NL.NL_42.42.NA' Invoke-sqmLogging -Message "UseDiff: Management-Klasse auf $ManagementClass gesetzt." -FunctionName $functionName -Level "INFO" } $result.ManagementClass = $ManagementClass # --- Backup-Verzeichnis ermitteln --- $effBackupDir = $BackupDirectory if (-not $effBackupDir) { try { $connParams = @{ SqlInstance = $SqlInstance } if ($SqlCredential) { $connParams['SqlCredential'] = $SqlCredential } $regResult = Invoke-DbaQuery @connParams -Query @" DECLARE @BackupDirectory NVARCHAR(4000); EXEC master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE', N'SOFTWARE\Microsoft\MSSQLServer\MSSQLServer', N'BackupDirectory', @BackupDirectory OUTPUT; SELECT @BackupDirectory AS BackupDirectory; "@ -ErrorAction Stop $effBackupDir = $regResult.BackupDirectory } catch { Invoke-sqmLogging -Message "SQL-Registry-Abfrage fehlgeschlagen: $($_.Exception.Message)" -FunctionName $functionName -Level "VERBOSE" } if (-not $effBackupDir) { try { $srv = Connect-DbaInstance @connParams -ErrorAction SilentlyContinue $effBackupDir = $srv.BackupDirectory } catch { } } } if (-not $effBackupDir) { $effBackupDir = 'C:\Program Files\Microsoft SQL Server\MSSQL\Backup' Invoke-sqmLogging -Message "Kein Backup-Verzeichnis ermittelbar - verwende Standard: $effBackupDir" -FunctionName $functionName -Level "WARNING" } $result.BackupDirectory = $effBackupDir Invoke-sqmLogging -Message "Backup-Verzeichnis: $effBackupDir" -FunctionName $functionName -Level "INFO" # --- dsm.opt-Pfad ermitteln --- $isLocal = $ComputerName -in @($env:COMPUTERNAME, 'localhost', '127.0.0.1', '.') $localDsmOpt = $DsmOptPath if (-not $localDsmOpt) { $localDsmOpt = _FindDsmOptPath -ComputerName $ComputerName -IsLocal $isLocal -Credential $Credential } if (-not $localDsmOpt) { $msg = "dsm.opt konnte nicht gefunden werden. Bitte -DsmOptPath explizit angeben." Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw $msg } $result.Status = 'DsmOptNotFound' $result.Message = $msg return $result } $result.DsmOptPath = $localDsmOpt $accessPath = if ($isLocal) { $localDsmOpt } else { _ToUncPath -ComputerName $ComputerName -LocalPath $localDsmOpt } Invoke-sqmLogging -Message "dsm.opt Zugriffspfad: $accessPath" -FunctionName $functionName -Level "VERBOSE" # --- dsm.opt lesen --- if (-not (Test-Path -Path $accessPath -ErrorAction SilentlyContinue)) { $msg = "dsm.opt nicht gefunden: $accessPath" Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw $msg } $result.Status = 'DsmOptNotFound' $result.Message = $msg return $result } $existingLines = [System.Collections.Generic.List[string]]::new() try { $rawLines = Get-Content -Path $accessPath -Encoding UTF8 -ErrorAction Stop foreach ($l in $rawLines) { $existingLines.Add($l) } Invoke-sqmLogging -Message "dsm.opt gelesen: $($existingLines.Count) Zeilen" -FunctionName $functionName -Level "INFO" } catch { $msg = "dsm.opt konnte nicht gelesen werden: $($_.Exception.Message)" Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw $msg } $result.Status = 'ReadFailed' $result.Message = $msg return $result } # --- Backup der dsm.opt --- $bakPath = $accessPath + '.bak' try { Copy-Item -Path $accessPath -Destination $bakPath -Force -ErrorAction Stop $result.BackupCreated = $true Invoke-sqmLogging -Message "Backup angelegt: $bakPath" -FunctionName $functionName -Level "INFO" } catch { $msg = "Backup der dsm.opt fehlgeschlagen: $($_.Exception.Message)" Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw $msg } $result.Status = 'BackupFailed' $result.Message = $msg return $result } # --- Include-Pfade vorbereiten --- $includePaths = [System.Collections.Generic.List[string]]::new() $includePaths.Add("$effBackupDir\User-db") $includePaths.Add("$effBackupDir\Sys-db") foreach ($p in $AdditionalIncludePaths) { if ($p -and $p.Trim()) { $includePaths.Add($p.TrimEnd('\')) } } # --- Verwaltungsblock erstellen --- $blockLines = [System.Collections.Generic.List[string]]::new() $blockLines.Add('') $blockLines.Add('* --- dtcSqlTools BEGIN ---') $blockLines.Add("* Konfiguriert von MSSQLTools Invoke-sqmTsmConfiguration") $blockLines.Add("* Zeitpunkt: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')") $blockLines.Add("* Server: $ComputerName | SQL-Backup: $effBackupDir") $blockLines.Add("* UseDiff: $UseDiff | ManagementClass: $ManagementClass") $blockLines.Add('*') $blockLines.Add('* SQL Server Datenbankdateien vom TSM-Backup ausschliessen:') $excludePatterns = @('EXCLUDE "*:\...\*.ldf"', 'EXCLUDE "*:\...\*.mdf"', 'EXCLUDE "*:\...\*.ndf"') foreach ($excl in $excludePatterns) { $blockLines.Add($excl) } $result.ExcludesWritten = $excludePatterns.Count $blockLines.Add('*') $blockLines.Add('* SQL Server Backup-Verzeichnisse einschliessen:') foreach ($incPath in $includePaths) { $blockLines.Add("INCLUDE `"$incPath\*`"") $result.IncludesWritten++ } $blockLines.Add('*') $blockLines.Add('* Management-Klasse fuer Backup-Dateien:') foreach ($incPath in $includePaths) { $blockLines.Add("INCLUDE `"$incPath\*`" $ManagementClass") } $blockLines.Add('*') $blockLines.Add('* --- dtcSqlTools END ---') $blockLines.Add('') # --- Vorhandenen Block entfernen / ersetzen --- $beginMarker = '* --- dtcSqlTools BEGIN ---' $endMarker = '* --- dtcSqlTools END ---' $beginIdx = -1; $endIdx = -1 for ($i = 0; $i -lt $existingLines.Count; $i++) { if ($existingLines[$i].Trim() -eq $beginMarker) { $beginIdx = $i } if ($existingLines[$i].Trim() -eq $endMarker) { $endIdx = $i } } $newLines = [System.Collections.Generic.List[string]]::new() if ($beginIdx -ge 0 -and $endIdx -gt $beginIdx) { for ($i = 0; $i -lt $beginIdx; $i++) { if ($i -eq $beginIdx - 1 -and $existingLines[$i].Trim() -eq '') { continue } $newLines.Add($existingLines[$i]) } foreach ($l in $blockLines) { $newLines.Add($l) } for ($i = $endIdx + 1; $i -lt $existingLines.Count; $i++) { $newLines.Add($existingLines[$i]) } Invoke-sqmLogging -Message "Vorhandenen dtcSqlTools-Block ersetzt." -FunctionName $functionName -Level "INFO" } else { foreach ($l in $existingLines) { $newLines.Add($l) } foreach ($l in $blockLines) { $newLines.Add($l) } Invoke-sqmLogging -Message "dtcSqlTools-Block am Ende der dsm.opt eingefuegt." -FunctionName $functionName -Level "INFO" } # --- Schreiben --- if ($PSCmdlet.ShouldProcess($accessPath, "dsm.opt schreiben")) { try { $newLines | Out-File -FilePath $accessPath -Encoding UTF8 -Force -ErrorAction Stop Invoke-sqmLogging -Message "dsm.opt geschrieben: $($newLines.Count) Zeilen" -FunctionName $functionName -Level "INFO" $result.Status = 'Success' $result.Message = "dsm.opt konfiguriert: $($result.ExcludesWritten) EXCLUDE(s), $($result.IncludesWritten) INCLUDE(s), ManagementClass: $ManagementClass" } catch { $msg = "dsm.opt konnte nicht geschrieben werden: $($_.Exception.Message)" Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw $msg } $result.Status = 'WriteFailed' $result.Message = $msg return $result } } else { $result.Status = 'WhatIf' $result.Message = "WhatIf: dsm.opt wuerde geschrieben werden ($($newLines.Count) Zeilen)." } # --- Bericht schreiben (optional) --- if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null } $datestamp = Get-Date -Format 'yyyy-MM-dd' $safeComp = $ComputerName -replace '[\\/:*?"<>|]', '_' $reportFile = Join-Path $OutputPath "TsmConfiguration_${safeComp}_${datestamp}.txt" $result.ReportPath = $reportFile @" # ================================================================ # MSSQLTools - TSM dsm.opt Konfigurationsbericht # Computer : $ComputerName # Datum : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') # dsm.opt : $accessPath # Backup-Pfad : $effBackupDir # ManagementClass: $ManagementClass # UseDiff : $UseDiff # Status : $($result.Status) # ================================================================ EXCLUDEs: $($excludePatterns -join "`n ") INCLUDEs: $($includePaths -join "`n ") ManagementClass-Zuweisungen: $($includePaths -join "`n ") "@ | Out-File -FilePath $reportFile -Encoding UTF8 -Force Copy-sqmToCentralPath -Path $reportFile Invoke-sqmLogging -Message "Bericht erstellt: $reportFile" -FunctionName $functionName -Level "INFO" } catch { $errMsg = $_.Exception.Message Invoke-sqmLogging -Message "Allgemeiner Fehler: $errMsg" -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $result.Status = 'Failed' $result.Message = $errMsg } return $result } end { Invoke-sqmLogging -Message "$functionName abgeschlossen." -FunctionName $functionName -Level "INFO" } } # --- Private Hilfsfunktionen (nicht exportiert) --- function _FindDsmOptPath { param ( [string]$ComputerName = "localhost", [bool]$IsLocal = $true, [System.Management.Automation.PSCredential]$Credential ) $candidates = [System.Collections.Generic.List[string]]::new() if ($IsLocal) { # Umgebungsvariablen pruefen if ($env:DSM_DIR) { $candidates.Add((Join-Path $env:DSM_DIR 'dsm.opt')) } if ($env:DSM_CONFIG) { $candidates.Add($env:DSM_CONFIG) } # Lokale Registry (64-Bit & 32-Bit) $regPaths = @( 'HKLM:\SOFTWARE\IBM\ADSM\CurrentVersion\BackupClient', 'HKLM:\SOFTWARE\WOW6432Node\IBM\ADSM\CurrentVersion\BackupClient' ) foreach ($rp in $regPaths) { $val = (Get-ItemProperty $rp -Name 'DSM_DIR' -ErrorAction SilentlyContinue).DSM_DIR if ($val) { $candidates.Add((Join-Path $val 'dsm.opt')) } } } else { try { $hklm = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $ComputerName) $regKeys = @( 'SOFTWARE\IBM\ADSM\CurrentVersion\BackupClient', 'SOFTWARE\WOW6432Node\IBM\ADSM\CurrentVersion\BackupClient' ) foreach ($keyPath in $regKeys) { $tsmKey = $hklm.OpenSubKey($keyPath) if ($tsmKey) { $regDsmDir = $tsmKey.GetValue('DSM_DIR', '') if ($regDsmDir) { $candidates.Add((Join-Path $regDsmDir 'dsm.opt')) } $tsmKey.Close() } } $hklm.Close() } catch { Write-Warning "Remote Registry Zugriff auf $ComputerName fehlgeschlagen: $($_.Exception.Message)" } } # Standardpfade hinzufuegen $candidates.Add('C:\Program Files\Tivoli\TSM\baclient\dsm.opt') $candidates.Add('C:\Program Files\IBM\TSM\baclient\dsm.opt') $candidates.Add('C:\Program Files\IBM\SpectrumProtect\baclient\dsm.opt') foreach ($c in ($candidates | Select-Object -Unique)) { if (-not $c) { continue } $testPath = if ($IsLocal) { $c } else { _ToUncPath -ComputerName $ComputerName -LocalPath $c } # Test-Path mit New-PSDrive kombinieren, falls Credentials noetig sind if (Test-Path -Path $testPath -ErrorAction SilentlyContinue) { return $c } } return $null } function _ToUncPath { param ([string]$ComputerName, [string]$LocalPath) # Entfernt Backslashes am Anfang fuer Join-Path Stabilitaet if ($LocalPath -match '^([A-Za-z]):\\(.*)$') { return "\\$ComputerName\$($Matches[1])`$\$($Matches[2])" } return "\\$ComputerName\$($LocalPath.Replace(':', '$'))" } |