bin/Public/Get-sqmSpnReport.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Prueft die gesetzten SPNs fuer SQL Server-Instanzen (Standard- und benannte Instanzen). .DESCRIPTION Ermittelt automatisch alle SQL Server-Dienste auf dem angegebenen Computer, bestimmt pro Instanz das Dienstkonto und leitet daraus das AD-Konto fuer die SPN-Pruefung ab. Unterstuetzte Dienstkonto-Typen: - Domaenenkonto (DOMAIN\svc_sql) ? direkt als SPN-Konto verwendet - Computerkonto-Konten (SYSTEM, NETWORK SERVICE, NT SERVICE\*) ? Computerkonto (DOMAIN\HOSTNAME$) Das Computerkonto wird sauber ueber [System.DirectoryServices.ActiveDirectory.Domain] ermittelt. - LOCAL SERVICE ? keine Netzwerkidentitaet, SPNs nicht moeglich ? Befund mit Status 'NoNetwork' Pro Instanz werden die vier erwarteten MSSQLSvc-SPNs geprueft: MSSQLSvc/<Hostname>:<Port> MSSQLSvc/<FQDN>:<Port> MSSQLSvc/<Hostname> (nur Standardinstanz, Port 1433) MSSQLSvc/<FQDN> (nur Standardinstanz, Port 1433) Bei benannten Instanzen (dynamischer Port via SQL Browser) werden zusaetzlich die Instanznamens-SPNs geprueft: MSSQLSvc/<Hostname>:<Instanzname> MSSQLSvc/<FQDN>:<Instanzname> Fuer AlwaysOn-Verfuegbarkeitsgruppen werden auch Listener-SPNs geprueft: MSSQLSvc/<ListenerName>:<Port> MSSQLSvc/<ListenerFQDN>:<Port> Fehlende SPNs werden als fertige setspn.exe-Kommandos aufbereitet, die an das AD-Team uebergeben werden koennen. Ausgabe pro Instanz: SpnReport_<Computer>_<Instanz>_<Datum>.txt - Lesbarer Bericht inkl. setspn-Kommandos SpnReport_<Computer>_<Instanz>_<Datum>.csv - Maschinenlesbar (eine Zeile pro SPN) .PARAMETER ComputerName Zielrechner. Standard: lokaler Computer. Pipeline-faehig. .PARAMETER InstanceFilter Optionaler Filter auf Instanznamen (Wildcards erlaubt). Beispiel: 'MSSQLSERVER' fuer nur die Standardinstanz, 'SQL*' fuer benannte Instanzen. .PARAMETER OutputPath Ausgabeverzeichnis fuer Bericht und CSV. Standard: Modulkonfiguration (Get-sqmConfig -Key 'OutputPath'). .PARAMETER ContinueOnError Bei Fehler auf einer Instanz mit der naechsten fortfahren. .PARAMETER EnableException Ausnahmen sofort ausloesen (ueberschreibt ContinueOnError). .PARAMETER Confirm Fordert vor dem Erstellen der Dateien eine Bestaetigung an. .PARAMETER WhatIf Zeigt, welche Dateien erstellt wuerden, ohne sie zu schreiben. .EXAMPLE Get-sqmSpnReport Prueft alle SQL Server-Instanzen auf dem lokalen Computer. .EXAMPLE Get-sqmSpnReport -ComputerName 'SQL01' -InstanceFilter 'MSSQLSERVER' Prueft nur die Standardinstanz auf SQL01. .EXAMPLE 'SQL01','SQL02' | Get-sqmSpnReport -ContinueOnError Prueft alle Instanzen auf zwei Servern, Fehler werden uebersprungen. .EXAMPLE $result = Get-sqmSpnReport -ComputerName 'SQL01' $result.DetailRows | Where-Object Status -eq 'Missing' | Select-Object Spn, SetSpnCommand Gibt nur fehlende SPNs mit dem fertigen setspn-Befehl aus. .NOTES Voraussetzungen: - Invoke-sqmLogging, Get-sqmConfig - setspn.exe im Systempfad (Windows-Standard oder RSAT) - Lokale Administratorrechte auf dem Zielcomputer fuer WMI-Abfragen - AD-Modul (RSAT) ist NICHT erforderlich; Domaenenermittlung erfolgt via [System.DirectoryServices.ActiveDirectory.Domain] #> function Get-sqmSpnReport { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'None')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [Alias('Computer', 'Server')] [string[]]$ComputerName = @($env:COMPUTERNAME), [Parameter(Mandatory = $false)] [string]$InstanceFilter = '*', [Parameter(Mandatory = $false)] [string]$OutputPath = (Get-sqmConfig -Key 'OutputPath'), [Parameter(Mandatory = $false)] [switch]$ContinueOnError, [Parameter(Mandatory = $false)] [switch]$EnableException ) begin { $functionName = $MyInvocation.MyCommand.Name $allResults = [System.Collections.Generic.List[PSCustomObject]]::new() # setspn.exe verfuegbar? $setspnCmd = Get-Command -Name 'setspn.exe' -ErrorAction SilentlyContinue if (-not $setspnCmd) { $errMsg = "setspn.exe nicht gefunden. RSAT oder Windows-Tools pruefen." Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level 'ERROR' throw $errMsg } Invoke-sqmLogging -Message "setspn.exe gefunden: $($setspnCmd.Source)" ` -FunctionName $functionName -Level 'INFO' Invoke-sqmLogging -Message "Starte $functionName | OutputPath: $OutputPath" ` -FunctionName $functionName -Level 'INFO' # ------------------------------------------------------------------ # Hilfsfunktion: Dienstkonto ? SPN-Konto + Konto-Typ bestimmen # # Rueckgabe: [PSCustomObject] mit # AccountType : 'Domain' | 'Computer' | 'NoNetwork' # SpnAccount : Das Konto, unter dem die SPNs registriert sind # Note : Erklaerungstext fuer den Bericht # ------------------------------------------------------------------ function _ResolveSpnAccount { param ( [string]$ServiceAccount, [string]$HostName ) # LOCAL SERVICE - keine Netzwerkidentitaet if ($ServiceAccount -match '(?i)(NT AUTHORITY\\LOCAL SERVICE|NT-AUTORIT[Aae]T\\LOKALER DIENST)') { return [PSCustomObject]@{ AccountType = 'NoNetwork' SpnAccount = $null Note = "Konto '$ServiceAccount' hat keine Netzwerkidentitaet. Kerberos/SPNs nicht moeglich. " + "Empfehlung: Dienstkonto auf Domaenenkonto, NETWORK SERVICE oder gMSA umstellen." } } # Eingebaute Konten die als Computerkonto im Netzwerk agieren: # NT AUTHORITY\SYSTEM / NT-AUTORITaeT\SYSTEM # NT AUTHORITY\NETWORK SERVICE / NT-AUTORITaeT\NETZWERKDIENST # NT SERVICE\<VirtualAccount> (z.B. NT SERVICE\MSSQLSERVER) # LocalSystem (Legacy-Schreibweise) $isBuiltinComputerAccount = $ServiceAccount -match ( '(?i)(' + 'LocalSystem' + '|' + 'NT AUTHORITY\\SYSTEM' + '|' + 'NT-AUTORIT[Aae]T\\SYSTEM' + '|' + 'NT AUTHORITY\\NETWORK SERVICE' + '|' + 'NT-AUTORIT[Aae]T\\NETZWERKDIENST' + '|' + 'NT SERVICE\\' + ')' ) if ($isBuiltinComputerAccount) { # Domaene sauber ueber .NET ermitteln - kein $env:USERDOMAIN # (der kann auf Mitgliedsservern den lokalen Computernamen liefern) $domainPrefix = $null try { $adDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() # NetBIOS-Name aus dem Distinguished Name ableiten ist nicht direkt # moeglich; zuverlaessigster Weg: LDAP-Abfrage auf das Computerobjekt # oder den NetBIOS-Namen via WMI holen. $wmiDomain = Get-CimInstance -ClassName Win32_ComputerSystem ` -ErrorAction Stop | Select-Object -ExpandProperty Domain # Win32_ComputerSystem.Domain liefert den FQDN der Domaene. # NetBIOS-Name via Win32_NTDomain (zuverlaessiger als USERDOMAIN): $ntDomain = Get-CimInstance -ClassName Win32_NTDomain ` -Filter "DnsForestName IS NOT NULL" ` -ErrorAction SilentlyContinue | Where-Object { $_.DnsForestName -and $_.DomainName } | Select-Object -First 1 -ExpandProperty DomainName $domainPrefix = if ($ntDomain) { $ntDomain } else { $wmiDomain.Split('.')[0].ToUpper() } } catch { # Fallback: erster Teil des COMPUTERNAME-FQDN oder USERDOMAIN $domainPrefix = if ($env:USERDNSDOMAIN) { $env:USERDNSDOMAIN.Split('.')[0].ToUpper() } else { $env:USERDOMAIN } Invoke-sqmLogging -Message "Domaenenermittlung via .NET fehlgeschlagen, Fallback: $domainPrefix. Fehler: $($_.Exception.Message)" ` -FunctionName $functionName -Level 'WARNING' } $spnAccount = "${domainPrefix}\${HostName}$" return [PSCustomObject]@{ AccountType = 'Computer' SpnAccount = $spnAccount Note = "Konto '$ServiceAccount' agiert als Computerkonto. SPNs werden unter '$spnAccount' geprueft." } } # Echtes Domaenenkonto (DOMAIN\user oder user@domain.local) # UPN-Format (user@domain.net) in SAM-Format (DOMAIN\user) konvertieren fuer setspn.exe $finalSpnAccount = $ServiceAccount if ($ServiceAccount -match '^([^@]+)@(.+)$') { # UPN-Format erkannt: user@domain.net $upnUser = $Matches[1] $upnDomain = $Matches[2] try { # .NET Directory Services nutzen um SAM-Namen zu ermitteln $userContext = New-Object System.DirectoryServices.DirectoryEntry $userSearcher = New-Object System.DirectoryServices.DirectorySearcher($userContext) $userSearcher.Filter = "(userPrincipalName=$ServiceAccount)" $result = $userSearcher.FindOne() if ($result) { # sAMAccountName auslesen $samName = $result.Properties['samaccountname'][0] # Domaenen-sAMAccountName bestimmen (z.B. DOMAIN\user) $domainName = $upnDomain.Split('.')[0].ToUpper() # z.B. "domain.net" -> "DOMAIN" $finalSpnAccount = "${domainName}\${samName}" Invoke-sqmLogging -Message "UPN '$ServiceAccount' in SAM-Format konvertiert: '$finalSpnAccount'" ` -FunctionName $functionName -Level 'INFO' } } catch { # Fallback: Nutze das erste Segment als Domain-Prefix $domainName = $upnDomain.Split('.')[0].ToUpper() $finalSpnAccount = "${domainName}\${upnUser}" Invoke-sqmLogging -Message "UPN-Konversion via .NET fehlgeschlagen, Fallback-Format: '$finalSpnAccount'. Fehler: $($_.Exception.Message)" ` -FunctionName $functionName -Level 'WARNING' } } return [PSCustomObject]@{ AccountType = 'Domain' SpnAccount = $finalSpnAccount Note = "Domaenenkonto '$ServiceAccount' wird als '$finalSpnAccount' fuer SPN-Pruefung verwendet." } } # ------------------------------------------------------------------ # Hilfsfunktion: Vorhandene MSSQLSvc-SPNs via setspn.exe einlesen # Rueckgabe: [string[]] der gefundenen SPNs, oder $null bei Fehler # ------------------------------------------------------------------ function _GetExistingSpns { param ([string]$SpnAccount) try { $raw = & setspn.exe -L $SpnAccount 2>&1 $text = $raw -join "`n" if ($text -match '(?i)(no such|Kein Objekt|cannot find|nicht gefunden|Object not found)') { return $null # Konto nicht in AD gefunden } $spns = $raw | Where-Object { $_ -match 'MSSQLSvc' } | ForEach-Object { $_.Trim() } return @($spns) } catch { throw "setspn.exe Ausfuehrungsfehler: $($_.Exception.Message)" } } } process { foreach ($computer in $ComputerName) { Invoke-sqmLogging -Message "[$computer] Beginne SPN-Pruefung ..." ` -FunctionName $functionName -Level 'INFO' # ------------------------------------------------------------------ # Hostname und FQDN des Zielcomputers ermitteln # ------------------------------------------------------------------ $hostName = $computer.ToUpper().Split('.')[0] # nur NetBIOS-Teil $fqdn = $computer try { $fqdn = [System.Net.Dns]::GetHostEntry($hostName).HostName } catch { Invoke-sqmLogging -Message "[$computer] FQDN-Aufloesung fehlgeschlagen, verwende '$hostName'. Fehler: $($_.Exception.Message)" ` -FunctionName $functionName -Level 'WARNING' $fqdn = $hostName } Invoke-sqmLogging -Message "[$computer] Hostname: $hostName | FQDN: $fqdn" ` -FunctionName $functionName -Level 'INFO' # ------------------------------------------------------------------ # Alle SQL Server-Dienste auf dem Computer via CIM ermitteln # Dienste: MSSQLSERVER (Standard) und MSSQL$<Instanzname> (benannt) # ------------------------------------------------------------------ $sqlServices = $null try { $isLocal = ($hostName -eq $env:COMPUTERNAME) $cimParams = @{ ClassName = 'Win32_Service'; ErrorAction = 'Stop' } if (-not $isLocal) { $cimSession = New-CimSession -ComputerName $hostName -ErrorAction Stop $cimParams['CimSession'] = $cimSession } $sqlServices = Get-CimInstance @cimParams | Where-Object { $_.Name -eq 'MSSQLSERVER' -or $_.Name -like 'MSSQL$*' } } catch { $errMsg = "[$computer] SQL Server-Dienste konnten nicht abgefragt werden: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level 'ERROR' $allResults.Add([PSCustomObject]@{ ComputerName = $computer InstanceName = '(alle)' Status = 'Error' Message = $errMsg DetailRows = $null TxtFile = $null CsvFile = $null }) if ($EnableException) { throw $errMsg } if (-not $ContinueOnError) { throw $errMsg } continue } if (-not $sqlServices) { $warnMsg = "[$computer] Keine SQL Server-Dienste gefunden." Invoke-sqmLogging -Message $warnMsg -FunctionName $functionName -Level 'WARNING' $allResults.Add([PSCustomObject]@{ ComputerName = $computer InstanceName = '(keine)' Status = 'Warning' Message = $warnMsg DetailRows = $null TxtFile = $null CsvFile = $null }) continue } # ------------------------------------------------------------------ # Pro SQL Server-Dienst # ------------------------------------------------------------------ foreach ($svc in $sqlServices) { # Instanzname ableiten: # MSSQLSERVER ? Standardinstanz # MSSQL$INSTANZNAME ? benannte Instanz $isDefaultInstance = ($svc.Name -eq 'MSSQLSERVER') $instanceName = if ($isDefaultInstance) { 'MSSQLSERVER' } else { $svc.Name -replace '^MSSQL\$', '' } # InstanceFilter anwenden if ($instanceName -notlike $InstanceFilter) { Invoke-sqmLogging -Message "[$computer\$instanceName] uebersprungen (InstanceFilter: '$InstanceFilter')." ` -FunctionName $functionName -Level 'VERBOSE' continue } Invoke-sqmLogging -Message "[$computer\$instanceName] Verarbeite Dienst '$($svc.Name)' ..." ` -FunctionName $functionName -Level 'INFO' $detailRows = [System.Collections.Generic.List[PSCustomObject]]::new() try { $serviceAccount = $svc.StartName Invoke-sqmLogging -Message "[$computer\$instanceName] Dienstkonto: $serviceAccount" ` -FunctionName $functionName -Level 'INFO' # -------------------------------------------------------- # Konto-Typ und SPN-Konto aufloesen # -------------------------------------------------------- $accountInfo = _ResolveSpnAccount -ServiceAccount $serviceAccount -HostName $hostName Invoke-sqmLogging -Message "[$computer\$instanceName] Konto-Typ: $($accountInfo.AccountType) | $($accountInfo.Note)" ` -FunctionName $functionName -Level 'INFO' # LOCAL SERVICE ? sofort als Befund ablegen, keine SPN-Pruefung if ($accountInfo.AccountType -eq 'NoNetwork') { $detailRows.Add([PSCustomObject]@{ ComputerName = $computer InstanceName = $instanceName ServiceName = $svc.Name ServiceAccount = $serviceAccount AccountType = $accountInfo.AccountType SpnAccount = $null Spn = $null Status = 'NoNetwork' SetSpnCommand = $null Note = $accountInfo.Note }) Invoke-sqmLogging -Message "[$computer\$instanceName] WARNUNG: $($accountInfo.Note)" ` -FunctionName $functionName -Level 'WARNING' # Bericht schreiben und naechste Instanz _WriteReport -Computer $computer -Instance $instanceName ` -ServiceAccount $serviceAccount -AccountInfo $accountInfo ` -HostName $hostName -Fqdn $fqdn ` -DetailRows $detailRows -OutputPath $OutputPath ` -AllResults $allResults -Status 'Warning' continue } $spnAccount = $accountInfo.SpnAccount # -------------------------------------------------------- # SQL Server-Port ermitteln # Standardinstanz: 1433 (oder konfiguriert) # Benannte Instanz: dynamisch aus Registry # -------------------------------------------------------- $sqlPort = 1433 try { # Registry-Pfad fuer TCP-Port: # HKLM\SOFTWARE\Microsoft\Microsoft SQL Server\<InstanceReg>\MSSQLServer\SuperSocketNetLib\Tcp\IPAll $regBase = 'SOFTWARE\Microsoft\Microsoft SQL Server' # Instanz-Registry-Schluesselname ermitteln via SqlInstanceNames $regInstKeyPath = "HKLM:\$regBase\Instance Names\SQL" $regInstKey = $null if ($isLocal) { $regInstKey = Get-ItemProperty -Path $regInstKeyPath -ErrorAction SilentlyContinue } else { $regInstKey = Invoke-Command -ComputerName $hostName -ScriptBlock { param($p) Get-ItemProperty -Path $p -ErrorAction SilentlyContinue } -ArgumentList $regInstKeyPath -ErrorAction SilentlyContinue } if ($regInstKey -and $regInstKey.$instanceName) { $instRegName = $regInstKey.$instanceName # z.B. MSSQL16.INSTANZNAME $tcpPath = "HKLM:\$regBase\$instRegName\MSSQLServer\SuperSocketNetLib\Tcp\IPAll" $tcpKey = $null if ($isLocal) { $tcpKey = Get-ItemProperty -Path $tcpPath -ErrorAction SilentlyContinue } else { $tcpKey = Invoke-Command -ComputerName $hostName -ScriptBlock { param($p) Get-ItemProperty -Path $p -ErrorAction SilentlyContinue } -ArgumentList $tcpPath -ErrorAction SilentlyContinue } if ($tcpKey) { # TcpPort gesetzt ? statischer Port; leer ? dynamischer Port (TcpDynamicPorts) $staticPort = $tcpKey.TcpPort $dynamicPort = $tcpKey.TcpDynamicPorts if ($staticPort -and $staticPort -ne '0' -and $staticPort -ne '') { $sqlPort = [int]$staticPort Invoke-sqmLogging -Message "[$computer\$instanceName] Statischer Port aus Registry: $sqlPort" ` -FunctionName $functionName -Level 'INFO' } elseif ($dynamicPort -and $dynamicPort -ne '0' -and $dynamicPort -ne '') { $sqlPort = [int]$dynamicPort Invoke-sqmLogging -Message "[$computer\$instanceName] Dynamischer Port aus Registry: $sqlPort" ` -FunctionName $functionName -Level 'INFO' } } } } catch { Invoke-sqmLogging -Message "[$computer\$instanceName] Port-Ermittlung aus Registry fehlgeschlagen, verwende 1433. Fehler: $($_.Exception.Message)" ` -FunctionName $functionName -Level 'WARNING' $sqlPort = 1433 } # -------------------------------------------------------- # Erwartete SPNs definieren # # Standardinstanz (MSSQLSERVER): # MSSQLSvc/<host>:1433 MSSQLSvc/<fqdn>:1433 # MSSQLSvc/<host> MSSQLSvc/<fqdn> # # Benannte Instanz: # MSSQLSvc/<host>:<port> MSSQLSvc/<fqdn>:<port> # MSSQLSvc/<host>:<instanz> MSSQLSvc/<fqdn>:<instanz> # -------------------------------------------------------- $expectedSpns = if ($isDefaultInstance) { @( "MSSQLSvc/${hostName}:${sqlPort}", "MSSQLSvc/${fqdn}:${sqlPort}", "MSSQLSvc/$hostName", "MSSQLSvc/$fqdn" ) } else { @( "MSSQLSvc/${hostName}:${sqlPort}", "MSSQLSvc/${fqdn}:${sqlPort}", "MSSQLSvc/${hostName}:${instanceName}", "MSSQLSvc/${fqdn}:${instanceName}" ) } Invoke-sqmLogging -Message "[$computer\$instanceName] Erwartete SPNs: $($expectedSpns -join ' | ')" ` -FunctionName $functionName -Level 'INFO' # -------------------------------------------------------- # Vorhandene SPNs via setspn.exe einlesen # -------------------------------------------------------- $existingSpns = _GetExistingSpns -SpnAccount $spnAccount if ($null -eq $existingSpns) { throw "Konto '$spnAccount' wurde in AD nicht gefunden (setspn -L lieferte kein Objekt)." } Invoke-sqmLogging -Message "[$computer\$instanceName] Vorhandene MSSQLSvc-SPNs ($($existingSpns.Count)): $($existingSpns -join ' | ')" ` -FunctionName $functionName -Level 'INFO' # -------------------------------------------------------- # Soll-/Ist-Vergleich ? Detailzeilen aufbauen # -------------------------------------------------------- foreach ($expectedSpn in $expectedSpns) { $isPresent = $existingSpns | Where-Object { $_ -ieq $expectedSpn } $status = if ($isPresent) { 'OK' } else { 'Missing' } $setSpnCmd = if (-not $isPresent) { "setspn -S $expectedSpn `"$spnAccount`"" } else { $null } $detailRows.Add([PSCustomObject]@{ ComputerName = $computer InstanceName = $instanceName ServiceName = $svc.Name ServiceAccount = $serviceAccount AccountType = $accountInfo.AccountType SpnAccount = $spnAccount Spn = $expectedSpn Status = $status SetSpnCommand = $setSpnCmd Note = $accountInfo.Note }) } # Zusaetzlich: bereits gesetzte SPNs die NICHT in der Erwartungsliste # stehen ? als 'Unexpected' melden (z.B. nach Portaenderung verwaiste SPNs) foreach ($existingSpn in $existingSpns) { $isExpected = $expectedSpns | Where-Object { $_ -ieq $existingSpn } if (-not $isExpected) { $detailRows.Add([PSCustomObject]@{ ComputerName = $computer InstanceName = $instanceName ServiceName = $svc.Name ServiceAccount = $serviceAccount AccountType = $accountInfo.AccountType SpnAccount = $spnAccount Spn = $existingSpn Status = 'Unexpected' SetSpnCommand = $null Note = "SPN vorhanden, aber nicht in Erwartungsliste (veraltet / Portaenderung?). Ggf. entfernen: setspn -D $existingSpn `"$spnAccount`"" }) } } # -------------------------------------------------------- # AlwaysOn Listener SPNs pruefen (falls Instanz in AG) # -------------------------------------------------------- try { $listenerQuery = @" SELECT ag.name AS AvailabilityGroupName, agl.dns_name AS ListenerDNSName, agl.port AS ListenerPort FROM sys.availability_groups ag INNER JOIN sys.availability_group_listeners agl ON ag.group_id = agl.group_id WHERE ag.group_id IN ( SELECT group_id FROM sys.dm_hadr_availability_replica_states WHERE is_local = 1 ) "@ $listeners = Invoke-DbaQuery @connParams -Query $listenerQuery -ErrorAction SilentlyContinue if ($listeners) { foreach ($listener in $listeners) { $listenerName = $listener.ListenerDNSName $listenerPort = $listener.ListenerPort $agName = $listener.AvailabilityGroupName # Listener FQDN ermitteln $listenerFqdn = $listenerName try { $listenerFqdn = [System.Net.Dns]::GetHostEntry($listenerName).HostName } catch { # Fallback: verwende DNS-Namen als-ist Invoke-sqmLogging -Message "[$computer\$instanceName] Listener FQDN-Aufloesung fuer '$listenerName' fehlgeschlagen, verwende DNS-Namen." ` -FunctionName $functionName -Level 'WARNING' } # Erwartete Listener-SPNs $listenerSpns = @( "MSSQLSvc/${listenerName}:${listenerPort}", "MSSQLSvc/${listenerFqdn}:${listenerPort}" ) Invoke-sqmLogging -Message "[$computer\$instanceName] AlwaysOn Listener '$listenerName' (AG: $agName) gefunden. Erwartete SPNs: $($listenerSpns -join ' | ')" ` -FunctionName $functionName -Level 'INFO' # Existierende Listener-SPNs pruefen (unter gleicher Service-Account) $existingListenerSpns = _GetExistingSpns -SpnAccount $spnAccount foreach ($listenerSpn in $listenerSpns) { $isPresent = $existingListenerSpns | Where-Object { $_ -ieq $listenerSpn } $status = if ($isPresent) { 'OK' } else { 'Missing' } $setSpnCmd = if (-not $isPresent) { "setspn -S $listenerSpn `"$spnAccount`"" } else { $null } $detailRows.Add([PSCustomObject]@{ ComputerName = $computer InstanceName = $instanceName ServiceName = $svc.Name ServiceAccount = $serviceAccount AccountType = $accountInfo.AccountType SpnAccount = $spnAccount Spn = $listenerSpn Status = $status SetSpnCommand = $setSpnCmd Note = "AlwaysOn Listener SPN (AG: $agName, Listener: $listenerName)" }) } # Unerwartete Listener-SPNs erkennen foreach ($existingSpn in $existingListenerSpns) { if ($existingSpn -match "^MSSQLSvc/" -and $existingSpn -notmatch ($listenerSpns -join '|')) { $isExpected = $expectedSpns | Where-Object { $_ -ieq $existingSpn } if (-not $isExpected -and $detailRows.Spn -notcontains $existingSpn) { $detailRows.Add([PSCustomObject]@{ ComputerName = $computer InstanceName = $instanceName ServiceName = $svc.Name ServiceAccount = $serviceAccount AccountType = $accountInfo.AccountType SpnAccount = $spnAccount Spn = $existingSpn Status = 'Unexpected' SetSpnCommand = $null Note = "AlwaysOn Listener SPN vorhanden, aber nicht erwartet (veraltet?). Ggf. entfernen: setspn -D $existingSpn `"$spnAccount`"" }) } } } } } } catch { Invoke-sqmLogging -Message "[$computer\$instanceName] AlwaysOn Listener-Pruefung fehlgeschlagen: $($_.Exception.Message)" ` -FunctionName $functionName -Level 'WARNING' # Fehler bei Listener-Pruefung blockiert nicht die Instanz-SPNs } $cntOk = ($detailRows | Where-Object Status -eq 'OK').Count $cntMissing = ($detailRows | Where-Object Status -eq 'Missing').Count $cntUnexpected = ($detailRows | Where-Object Status -eq 'Unexpected').Count Invoke-sqmLogging -Message "[$computer\$instanceName] OK: $cntOk | Fehlend: $cntMissing | Unerwartet: $cntUnexpected" ` -FunctionName $functionName -Level 'INFO' if ($cntMissing -gt 0) { Invoke-sqmLogging -Message "[$computer\$instanceName] $cntMissing SPN(s) fehlen - setspn-Kommandos im Bericht." ` -FunctionName $functionName -Level 'WARNING' } if ($cntUnexpected -gt 0) { Invoke-sqmLogging -Message "[$computer\$instanceName] $cntUnexpected unerwartete SPN(s) vorhanden." ` -FunctionName $functionName -Level 'WARNING' } # -------------------------------------------------------- # Gesamtstatus der Instanz # -------------------------------------------------------- $instanceStatus = if ($cntMissing -gt 0 -or $cntUnexpected -gt 0) { 'Warning' } else { 'OK' } # -------------------------------------------------------- # Bericht schreiben # -------------------------------------------------------- $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $datestamp = Get-Date -Format 'yyyy-MM-dd' $safeComp = $computer -replace '[\\/:*?"<>|]', '_' $safeInst = $instanceName -replace '[\\/:*?"<>|]', '_' $txtFile = Join-Path $OutputPath "SpnReport_${safeComp}_${safeInst}_${datestamp}.txt" $csvFile = Join-Path $OutputPath "SpnReport_${safeComp}_${safeInst}_${datestamp}.csv" if ($PSCmdlet.ShouldProcess("$computer\$instanceName", "Erstelle SPN-Bericht in $OutputPath")) { 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 = [System.Collections.Generic.List[string]]::new() $sep = '=' * 70 $dash = '-' * 70 $lines.Add($sep) $lines.Add("# sqmSQLTool - SPN-Pruefbericht") $lines.Add("# Computer : $computer") $lines.Add("# Hostname : $hostName") $lines.Add("# FQDN : $fqdn") $lines.Add("# Instanz : $instanceName") $lines.Add("# Dienst : $($svc.Name)") $lines.Add("# Dienstkonto : $serviceAccount") $lines.Add("# Konto-Typ : $($accountInfo.AccountType)") $lines.Add("# SPN-Konto (AD) : $spnAccount") $lines.Add("# SQL-Port : $sqlPort") $lines.Add("# Erstellt : $timestamp") $lines.Add("# Status : $instanceStatus") $lines.Add("# OK : $cntOk") $lines.Add("# Fehlend : $cntMissing") $lines.Add("# Unerwartet : $cntUnexpected") $lines.Add($sep) # Hinweis bei Computerkonto if ($accountInfo.AccountType -eq 'Computer') { $lines.Add("") $lines.Add("# Hinweis: Dienstkonto '$serviceAccount' agiert als Computerkonto.") $lines.Add("# SPNs werden unter '$spnAccount' geprueft und registriert.") } # Ergebnis: OK-SPNs $okSpns = $detailRows | Where-Object Status -eq 'OK' $lines.Add("") $lines.Add($dash) $lines.Add("# VORHANDENE SPNs ($cntOk / 4 erwartet)") $lines.Add($dash) if ($okSpns) { foreach ($r in $okSpns) { $lines.Add(" ? $($r.Spn)") } } else { $lines.Add(" (keine der erwarteten SPNs vorhanden)") } # Ergebnis: Fehlende SPNs + setspn-Kommandos $missingSpns = $detailRows | Where-Object Status -eq 'Missing' $lines.Add("") $lines.Add($dash) $lines.Add("# FEHLENDE SPNs ($cntMissing) ? AKTION ERFORDERLICH") $lines.Add($dash) if ($missingSpns) { $lines.Add("") $lines.Add(" Bitte folgende Kommandos als Domain-Admin ausfuehren:") $lines.Add(" (Parameter -S prueft auf Duplikate, bevorzugt gegenueber -A)") $lines.Add("") foreach ($r in $missingSpns) { $lines.Add(" ? $($r.Spn)") $lines.Add(" $($r.SetSpnCommand)") $lines.Add("") } $lines.Add(" Pruefung nach dem Setzen:") $lines.Add(" setspn -L `"$spnAccount`"") $lines.Add("") $lines.Add(" Berechtigung: Schreibrecht auf das Konto '$spnAccount' erforderlich.") } else { $lines.Add(" (keine fehlenden SPNs - kein Handlungsbedarf)") } # Unerwartete SPNs $unexpectedSpns = $detailRows | Where-Object Status -eq 'Unexpected' $lines.Add("") $lines.Add($dash) $lines.Add("# UNERWARTETE SPNs ($cntUnexpected) ? PRueFEN (ggf. veraltet)") $lines.Add($dash) if ($unexpectedSpns) { foreach ($r in $unexpectedSpns) { $lines.Add(" ? $($r.Spn)") $lines.Add(" $($r.Note)") $lines.Add("") } } else { $lines.Add(" (keine unerwarteten SPNs)") } $lines.Add("") $lines.Add($sep) $lines.Add("# Logdatei : $txtFile") $lines.Add("# CSV : $csvFile") $lines.Add($sep) $lines | Out-File -FilePath $txtFile -Encoding UTF8 -Force $detailRows | Export-Csv -Path $csvFile -Encoding UTF8 -NoTypeInformation -Force Invoke-sqmLogging -Message "[$computer\$instanceName] Bericht erstellt: $txtFile" ` -FunctionName $functionName -Level 'INFO' } else { Invoke-sqmLogging -Message "[$computer\$instanceName] WhatIf: Berichtsdateien wuerden erstellt werden." ` -FunctionName $functionName -Level 'VERBOSE' $txtFile = $null $csvFile = $null } $allResults.Add([PSCustomObject]@{ ComputerName = $computer InstanceName = $instanceName ServiceName = $svc.Name ServiceAccount = $serviceAccount AccountType = $accountInfo.AccountType SpnAccount = $spnAccount SqlPort = $sqlPort CountOk = $cntOk CountMissing = $cntMissing CountUnexpected = $cntUnexpected Status = $instanceStatus Message = if ($instanceStatus -eq 'OK') { "Alle SPNs korrekt gesetzt." } else { "$cntMissing fehlende, $cntUnexpected unerwartete SPN(s). Bericht pruefen." } DetailRows = $detailRows TxtFile = $txtFile CsvFile = $csvFile }) } catch { $errMsg = "[$computer\$instanceName] Fehler: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level 'ERROR' $allResults.Add([PSCustomObject]@{ ComputerName = $computer InstanceName = $instanceName ServiceName = $svc.Name ServiceAccount = $svc.StartName AccountType = 'Error' SpnAccount = $null SqlPort = $null CountOk = 0 CountMissing = 0 CountUnexpected = 0 Status = 'Error' Message = $errMsg DetailRows = $detailRows TxtFile = $null CsvFile = $null }) if ($EnableException) { throw } if (-not $ContinueOnError) { throw $_ } } } # foreach $svc } # foreach $computer } end { Invoke-sqmLogging -Message "$functionName abgeschlossen. $($allResults.Count) Instanz(en) verarbeitet." ` -FunctionName $functionName -Level 'INFO' return $allResults } } |