bin/Public/Invoke-sqmInstanceInventory.ps1
|
<#
.SYNOPSIS Erstellt eine vollstaendige Inventarisierung einer SQL Server-Instanz als strukturierten Bericht (TXT + CSV). .DESCRIPTION Dokumentiert folgende Bereiche: - Instanz (Version, Edition, Patch-Level, Collation, Speicher, CPU, sp_configure) - Datenbanken (Name, Status, Recovery, Groesse, letzte Backups, Owner, Collation) - Logins (Name, Typ, Status, Serverrollen) - Linked Server - SQL Agent Jobs (Name, Status, Owner, Schedules, letzte Ausfuehrung) - Always On (AGs, Replikate, Listener) Die Ausgabe erfolgt als: - TXT-Datei mit lesbarem Bericht - CSV-Datei mit der Datenbankliste Standard-Ausgabepfad wird aus der Modulkonfiguration (OutputPath) gelesen. Wenn konfiguriert, werden die Dateien zusaetzlich in den CentralPath kopiert. .PARAMETER SqlInstance SQL Server-Instanz(en). Pipeline-faehig. Standard: aktueller Computername. .PARAMETER SqlCredential Optionaler PSCredential fuer die Verbindung. .PARAMETER OutputPath Ausgabeverzeichnis fuer die Berichtsdateien. Standard: Wert aus Modulkonfiguration (Get-sqmDefaultOutputPath). .PARAMETER ContinueOnError Bei Fehler auf einer Instanz fortfahren (sonst Abbruch). .PARAMETER EnableException Ausnahmen durchlassen (fuer erweiterte Fehlerbehandlung). .PARAMETER Confirm Bestaetigung vor der Erstellung anfordern. .PARAMETER WhatIf Nur testen, keine Dateien schreiben. .EXAMPLE Invoke-sqmInstanceInventory .EXAMPLE Invoke-sqmInstanceInventory -SqlInstance "SQL01", "SQL02" -ContinueOnError .NOTES Erfordert dbatools und Invoke-sqmLogging. Die Funktion erstellt automatisch das Ausgabeverzeichnis, falls es nicht existiert. #> function Invoke-sqmInstanceInventory { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'None')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [string[]]$SqlInstance = @($env:COMPUTERNAME), [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$SqlCredential, [Parameter(Mandatory = $false)] [string]$OutputPath = (Get-sqmDefaultOutputPath), [Parameter(Mandatory = $false)] [switch]$ContinueOnError, [Parameter(Mandatory = $false)] [switch]$EnableException ) begin { $functionName = $MyInvocation.MyCommand.Name $allResults = [System.Collections.Generic.List[PSCustomObject]]::new() if (-not (Get-Module -ListAvailable -Name dbatools)) { $errMsg = "dbatools-Modul nicht gefunden." Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" throw $errMsg } Invoke-sqmLogging -Message "Starte $functionName mit OutputPath: $OutputPath" -FunctionName $functionName -Level "INFO" } process { foreach ($instance in $SqlInstance) { $connParams = @{ SqlInstance = $instance } if ($SqlCredential) { $connParams['SqlCredential'] = $SqlCredential } try { Invoke-sqmLogging -Message "[$instance] Starte Inventarisierung ..." -FunctionName $functionName -Level "INFO" $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $datestamp = Get-Date -Format 'yyyy-MM-dd' # Verbindung herstellen $srv = Connect-DbaInstance @connParams -ErrorAction Stop # Datenbanken $databases = Get-DbaDatabase @connParams -ErrorAction SilentlyContinue # Letzte Backups $backupQuery = @" SELECT database_name, type, MAX(backup_finish_date) AS LastBackup, SUM(backup_size)/1048576.0 AS AvgSizeMB FROM msdb.dbo.backupset WHERE type IN ('D','L') AND is_copy_only = 0 GROUP BY database_name, type; "@ $backupRows = Invoke-DbaQuery @connParams -Query $backupQuery -EnableException:$EnableException -ErrorAction SilentlyContinue $backupLookup = @{ } foreach ($r in $backupRows) { $backupLookup["$($r.database_name)|$($r.type)"] = $r.LastBackup } # Logins $logins = Get-DbaLogin @connParams -ErrorAction SilentlyContinue # Serverrollen pro Login $roleQuery = @" SELECT m.name AS LoginName, r.name AS RoleName FROM sys.server_role_members rm JOIN sys.server_principals m ON m.principal_id = rm.member_principal_id JOIN sys.server_principals r ON r.principal_id = rm.role_principal_id WHERE m.type IN ('S','U','G') ORDER BY m.name, r.name; "@ $roleRows = Invoke-DbaQuery @connParams -Query $roleQuery -EnableException:$EnableException -ErrorAction SilentlyContinue $roleLookup = @{ } foreach ($r in $roleRows) { if (-not $roleLookup[$r.LoginName]) { $roleLookup[$r.LoginName] = [System.Collections.Generic.List[string]]::new() } $roleLookup[$r.LoginName].Add($r.RoleName) } # Linked Server $linkedServers = Get-DbaLinkedServer @connParams -ErrorAction SilentlyContinue # SQL Agent Jobs $jobs = Get-DbaAgentJob @connParams -ErrorAction SilentlyContinue # sp_configure Abweichungen $configQuery = @" SELECT name, value_in_use, description FROM sys.configurations WHERE value_in_use <> minimum AND name NOT IN ('user connections','fill factor (%)','locks') ORDER BY name; "@ $configRows = Invoke-DbaQuery @connParams -Query $configQuery -EnableException:$EnableException -ErrorAction SilentlyContinue # Always On $ags = Get-DbaAvailabilityGroup @connParams -ErrorAction SilentlyContinue $listeners = if ($ags) { Get-DbaAgListener @connParams -ErrorAction SilentlyContinue } # ========== TXT-Bericht aufbauen ========== $lines = [System.Collections.Generic.List[string]]::new() $lines.Add("# ================================================================") $lines.Add("# MSSQLTools - Instanz-Inventar") $lines.Add("# Instanz : $instance") $lines.Add("# Erstellt : $timestamp") $lines.Add("# ================================================================") # --- Instanz-Info --- $lines.Add(""); $lines.Add("# ?? INSTANZ ??????????????????????????????????????????????????") $lines.Add((" Version : {0}" -f $srv.Version)) $lines.Add((" Edition : {0}" -f $srv.Edition)) $lines.Add((" Product Level : {0}" -f $srv.ProductLevel)) $lines.Add((" Product Update : {0}" -f $srv.ProductUpdateLevel)) $lines.Add((" Collation : {0}" -f $srv.Collation)) $lines.Add((" Auth-Modus : {0}" -f $srv.LoginMode)) $lines.Add((" Max Memory (MB) : {0}" -f $srv.Configuration.MaxServerMemory.ConfigValue)) $lines.Add((" Min Memory (MB) : {0}" -f $srv.Configuration.MinServerMemory.ConfigValue)) $lines.Add((" CPUs (logisch) : {0}" -f $srv.Processors)) $lines.Add((" MAXDOP : {0}" -f $srv.Configuration.MaxDegreeOfParallelism.ConfigValue)) $lines.Add((" BackupDirectory : {0}" -f $srv.BackupDirectory)) $lines.Add((" DefaultDataPath : {0}" -f $srv.DefaultFile)) $lines.Add((" DefaultLogPath : {0}" -f $srv.DefaultLog)) # --- Datenbanken --- $lines.Add(""); $lines.Add("# ?? DATENBANKEN ($(@($databases).Count)) ????????????????????????????????????") $lines.Add(("{0,-35} {1,-10} {2,-12} {3,-8} {4,-10} {5,-20} {6}" -f 'Name', 'Status', 'Recovery', 'SizeMB', 'CompatLvl', 'Letztes Full', 'Owner')) $lines.Add(("-" * 120)) $dbCsvList = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($db in ($databases | Sort-Object IsSystemObject, Name)) { $lastFull = $backupLookup["$($db.Name)|D"] $fullStr = if ($lastFull) { $lastFull.ToString('yyyy-MM-dd HH:mm') } else { '(keins)' } $lines.Add(("{0,-35} {1,-10} {2,-12} {3,-8} {4,-10} {5,-20} {6}" -f $db.Name.Substring(0, [Math]::Min(35, $db.Name.Length)), $db.Status, $db.RecoveryModel, [math]::Round($db.Size, 0), $db.CompatibilityLevel, $fullStr, $db.Owner)) $dbCsvList.Add([PSCustomObject]@{ SqlInstance = $instance DatabaseName = $db.Name Status = $db.Status RecoveryModel = $db.RecoveryModel SizeMB = [math]::Round($db.Size, 0) CompatibilityLevel = $db.CompatibilityLevel Owner = $db.Owner Collation = $db.Collation IsSystemObject = $db.IsSystemObject LastFullBackup = $fullStr CreateDate = $db.CreateDate.ToString('yyyy-MM-dd') }) } # --- Logins --- $lines.Add(""); $lines.Add("# ?? LOGINS ($(@($logins).Count)) ??????????????????????????????????????????") $lines.Add(("{0,-45} {1,-18} {2,-8} {3}" -f 'Name', 'Typ', 'Enabled', 'Serverrollen')) $lines.Add(("-" * 100)) foreach ($l in ($logins | Sort-Object LoginType, Name)) { $roles = if ($roleLookup[$l.Name]) { $roleLookup[$l.Name] -join ', ' } else { '-' } $lines.Add(("{0,-45} {1,-18} {2,-8} {3}" -f $l.Name.Substring(0, [Math]::Min(45, $l.Name.Length)), $l.LoginType, (-not $l.IsDisabled), $roles)) } # --- Linked Server --- if ($linkedServers) { $lines.Add(""); $lines.Add("# ?? LINKED SERVER ($(@($linkedServers).Count)) ?????????????????????????????????????") $lines.Add(("{0,-30} {1,-20} {2,-20} {3}" -f 'Name', 'Produkt', 'Provider', 'Datenquelle')) $lines.Add(("-" * 100)) foreach ($ls in ($linkedServers | Sort-Object Name)) { $lines.Add(("{0,-30} {1,-20} {2,-20} {3}" -f $ls.Name.Substring(0, [Math]::Min(30, $ls.Name.Length)), $ls.ProductName, $ls.ProviderName, $ls.DataSource)) } } # --- SQL Agent Jobs --- if ($jobs) { $lines.Add(""); $lines.Add("# ?? SQL AGENT JOBS ($(@($jobs).Count)) ??????????????????????????????????????") $lines.Add(("{0,-45} {1,-8} {2,-25} {3,-20} {4}" -f 'Name', 'Enabled', 'Owner', 'Letzte Ausfuehrung', 'Status')) $lines.Add(("-" * 120)) foreach ($j in ($jobs | Sort-Object IsEnabled, Name)) { $lastRun = if ($j.LastRunDate -and $j.LastRunDate.Year -gt 1990) { $j.LastRunDate.ToString('yyyy-MM-dd HH:mm') } else { '(nie)' } $lines.Add(("{0,-45} {1,-8} {2,-25} {3,-20} {4}" -f $j.Name.Substring(0, [Math]::Min(45, $j.Name.Length)), $j.IsEnabled, $j.OwnerLoginName.Substring(0, [Math]::Min(25, $j.OwnerLoginName.Length)), $lastRun, $j.LastRunOutcome)) } } # --- sp_configure Abweichungen --- if ($configRows) { $lines.Add(""); $lines.Add("# ?? KONFIGURATION (Abweichungen vom Standard) ????????????????") foreach ($c in $configRows) { $lines.Add((" {0,-45} = {1}" -f $c.name, $c.value_in_use)) } } # --- Always On --- if ($ags) { $lines.Add(""); $lines.Add("# ?? ALWAYS ON ($(@($ags).Count) AG(s)) ??????????????????????????????????????") foreach ($ag in $ags) { $lines.Add((" AG: $($ag.Name) | Primary: $($ag.PrimaryReplica) | " + "AutomatedBackup: $($ag.AutomatedBackupPreference)")) $replicas = Get-DbaAgReplica @connParams -AvailabilityGroup $ag.Name -ErrorAction SilentlyContinue foreach ($r in $replicas) { $lines.Add((" Replikat: {0,-30} Rolle: {1,-10} Mode: {2}" -f $r.Name, $r.Role, $r.AvailabilityMode)) } $agListeners = $listeners | Where-Object AvailabilityGroup -eq $ag.Name foreach ($l in $agListeners) { $lines.Add((" Listener: $($l.Name) Port: $($l.PortNumber) IPs: $($l.IpAddress -join ', ')")) } } } # --- Dateien schreiben (nur wenn -WhatIf nicht aktiv) --- $safeInst = $instance -replace '[\\/:*?"<>|]', '_' $txtFile = Join-Path $OutputPath "InstanceInventory_${safeInst}_${datestamp}.txt" $csvFile = Join-Path $OutputPath "InstanceInventory_${safeInst}_${datestamp}.csv" if ($PSCmdlet.ShouldProcess($instance, "Erstelle Inventar-Dateien in $OutputPath")) { # Verzeichnis anlegen if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force -ErrorAction Stop | Out-Null Invoke-sqmLogging -Message "Verzeichnis $OutputPath wurde erstellt." -FunctionName $functionName -Level "INFO" } $lines | Out-File -FilePath $txtFile -Encoding UTF8 -Force $dbCsvList | Export-Csv -Path $csvFile -Encoding UTF8 -NoTypeInformation -Force # In zentrales Verzeichnis kopieren (falls konfiguriert) Copy-sqmToCentralPath -Path $txtFile, $csvFile Invoke-sqmLogging -Message "[$instance] Inventar erstellt: $txtFile" -FunctionName $functionName -Level "INFO" } else { Invoke-sqmLogging -Message "[$instance] WhatIf: Dateien wuerden erstellt werden." -FunctionName $functionName -Level "VERBOSE" $txtFile = $null $csvFile = $null } # Ergebnisobjekt $result = [PSCustomObject]@{ SqlInstance = $instance Version = $srv.Version.ToString() Edition = $srv.Edition DatabaseCount = @($databases).Count LoginCount = @($logins).Count JobCount = @($jobs).Count AgCount = @($ags).Count TxtFile = $txtFile CsvFile = $csvFile Status = 'OK' Timestamp = $timestamp } $allResults.Add($result) } catch { $errMsg = "Fehler auf '$instance': $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" $allResults.Add([PSCustomObject]@{ SqlInstance = $instance Status = 'Error' TxtFile = $null CsvFile = $null Message = $errMsg }) if (-not $ContinueOnError -and -not $EnableException) { throw } if ($EnableException) { throw $_ } } } } end { Invoke-sqmLogging -Message "$functionName abgeschlossen. $($allResults.Count) Instanzen verarbeitet." -FunctionName $functionName -Level "INFO" return $allResults } } |