Public/Invoke-sqmSplunkConfiguration.ps1

# ---------------------------------------------------------------------------
# Private Hilfsfunktionen fuer Invoke-sqmSplunkConfiguration
# Script-Scope, nicht exportiert (kein -sqm im Namen)
# ---------------------------------------------------------------------------

# Schreibt Nachricht in Logdatei und Konsole (lokale Verwendung in LocalCore)
function _sqmSplunkWriteLog {
    param([string]$LogFile, [string]$Msg)
    $entry = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Msg"
    Add-Content -Path $LogFile -Value $entry -Encoding UTF8
    Write-Host $entry
}

# Gibt Nachricht an GUI-Callback oder Write-Host aus (aeussere Funktionen)
function _sqmSplunkGuiLog {
    param([string]$Message, [ScriptBlock]$LogCallback)
    if ($LogCallback) { & $LogCallback $Message }
    else              { Write-Host $Message }
}

# Lokale Kernlogik: SQL-Instanzen ermitteln, Env-Vars setzen, Dienst verwalten.
# Wird als String serialisiert und via Invoke-Command remote ausgefuehrt.
# Darf KEINE externen Abhaengigkeiten haben.
# Verwendet _sqmSplunkWriteLog - wird zusammen mit dieser Funktion serialisiert.
function _sqmSplunk_LocalCore {
    param([string]$LogPath, [bool]$TestMode)

    $ErrorActionPreference = 'Continue'

    if (-not (Test-Path $LogPath)) {
        $null = New-Item -ItemType Directory -Path $LogPath -Force
    }
    $ts      = Get-Date -Format 'yyyyMMdd_HHmmss'
    $logFile = Join-Path $LogPath "SplunkConfig_$ts.log"

    if (-not $TestMode) {
        $id = [Security.Principal.WindowsIdentity]::GetCurrent()
        $pr = New-Object Security.Principal.WindowsPrincipal($id)
        if (-not $pr.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
            Write-Host 'FEHLER: Administratorrechte erforderlich (Set-Modus).'
            return
        }
    }

    $modeLabel = if ($TestMode) { 'Test' } else { 'Set' }
    _sqmSplunkWriteLog $logFile "=== Invoke-sqmSplunkConfiguration | Modus: $modeLabel | $(hostname) ==="
    _sqmSplunkWriteLog $logFile "Logdatei: $logFile"

    $instKey = 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL'
    if (-not (Test-Path $instKey)) {
        _sqmSplunkWriteLog $logFile 'Keine SQL Server-Instanzen gefunden.'
        return
    }

    $instances     = Get-ItemProperty -Path $instKey
    $instanceNames = $instances.PSObject.Properties |
                     Where-Object { $_.Name -notin @('PSPath','PSParentPath','PSChildName','PSDrive','PSProvider') } |
                     Select-Object -ExpandProperty Name

    if ($instanceNames.Count -eq 0) {
        _sqmSplunkWriteLog $logFile 'Keine Instanzen in der Registry eingetragen.'
        return
    }

    _sqmSplunkWriteLog $logFile "Instanzen gefunden: $($instanceNames.Count) ($($instanceNames -join ', '))"

    $i = 1
    foreach ($instName in $instanceNames) {
        $instID  = $instances.$instName
        $varName = "MSSQL${i}_Log"

        if ($TestMode) {
            $existing = [Environment]::GetEnvironmentVariable($varName, [EnvironmentVariableTarget]::Machine)
            if ($null -eq $existing) {
                _sqmSplunkWriteLog $logFile "TEST: '$varName' nicht gesetzt."
            } else {
                _sqmSplunkWriteLog $logFile "TEST: '$varName' = '$existing'"
            }
            $i++
            continue
        }

        _sqmSplunkWriteLog $logFile "Verarbeite Instanz $i : $instName (ID: $instID)"

        $current = [Environment]::GetEnvironmentVariable($varName, [EnvironmentVariableTarget]::Machine)
        if ($null -ne $current) {
            _sqmSplunkWriteLog $logFile " '$varName' bereits gesetzt ('$current') - wird nicht ueberschrieben."
            $i++
            continue
        }

        try {
            $logDir = $null

            try {
                $asm = [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.Smo')
                if ($asm) {
                    $srvName = if ($instName -eq 'MSSQLSERVER') { '(local)' } else { "(local)\$instName" }
                    $srv    = New-Object Microsoft.SqlServer.Management.Smo.Server($srvName)
                    $logDir = $srv.ErrorLogPath
                }
            } catch { }

            if (-not $logDir) {
                $regP = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$instID\MSSQLServer\Parameters"
                if (Test-Path $regP) {
                    $prm = Get-ItemProperty -Path $regP
                    $arg = ($prm.PSObject.Properties |
                            Where-Object { $_.Name -like 'SQLArg*' -and $_.Value -like '-e*' }).Value
                    if ($arg) {
                        $logDir = Split-Path ($arg -replace '^-e"?','' -replace '"$','')
                    }
                }
            }

            if (-not $logDir) { throw 'Pfad nicht ermittelbar.' }

            _sqmSplunkWriteLog $logFile " ErrorLog-Pfad: $logDir"
            [Environment]::SetEnvironmentVariable($varName, $logDir, [EnvironmentVariableTarget]::Machine)
            _sqmSplunkWriteLog $logFile " OK: '$varName' = '$logDir' gesetzt."
        } catch {
            _sqmSplunkWriteLog $logFile " FEHLER bei $instName : $_"
        }

        $i++
    }

    $svcName = 'SplunkForwarder'
    $svc     = Get-Service -Name $svcName -ErrorAction SilentlyContinue

    if (-not $svc) {
        _sqmSplunkWriteLog $logFile "WARN: Dienst '$svcName' nicht vorhanden."
    } elseif ($TestMode) {
        _sqmSplunkWriteLog $logFile "TEST: Dienst '$svcName' Status = $($svc.Status)"
        if ($svc.Status -ne 'Running') {
            try {
                Start-Service -Name $svcName -ErrorAction Stop
                _sqmSplunkWriteLog $logFile 'TEST: Dienst gestartet.'
            } catch {
                _sqmSplunkWriteLog $logFile "TEST: FEHLER beim Starten: $_"
            }
        }
    } else {
        if ($svc.Status -eq 'Running') {
            try {
                Restart-Service -Name $svcName -Force
                _sqmSplunkWriteLog $logFile "Dienst '$svcName' neu gestartet."
            } catch {
                _sqmSplunkWriteLog $logFile "FEHLER beim Neustart: $_"
            }
        } else {
            try {
                Start-Service -Name $svcName -ErrorAction Stop
                _sqmSplunkWriteLog $logFile "Dienst '$svcName' gestartet."
            } catch {
                _sqmSplunkWriteLog $logFile "WARN: '$svcName' nicht laufend - Start fehlgeschlagen: $_"
            }
        }
    }

    _sqmSplunkWriteLog $logFile '=== Invoke-sqmSplunkConfiguration Ende ==='
}

# Remote-Engine: fuehrt _sqmSplunk_LocalCore auf mehreren Rechnern aus
function _sqmSplunk_OnComputers {
    param(
        [string[]]$ComputerNames,
        [string]$Mode,
        [string]$LogPath,
        [System.Management.Automation.PSCredential]$Credential,
        [ScriptBlock]$LogCallback
    )

    if ($ComputerNames.Count -eq 0) {
        _sqmSplunkGuiLog 'Keine Computer angegeben.' $LogCallback
        return
    }

    _sqmSplunkGuiLog "$($ComputerNames.Count) Computer werden verarbeitet..." $LogCallback

    # Beide Funktionen serialisieren - _sqmSplunk_LocalCore benoetigt _sqmSplunkWriteLog remote
    $coreStr    = ${function:_sqmSplunk_LocalCore}.ToString()
    $writeStr   = ${function:_sqmSplunkWriteLog}.ToString()
    $testMode   = ($Mode -eq 'Test')
    $combined   = "function _sqmSplunkWriteLog {$writeStr} ; function _sqmSplunk_LocalCore {$coreStr}"

    $remoteBlock = {
        param([string]$LogPath, [bool]$TestMode, [string]$Combined)
        . ([ScriptBlock]::Create($Combined))
        _sqmSplunk_LocalCore -LogPath $LogPath -TestMode $TestMode
    }

    $splat = @{
        ScriptBlock  = $remoteBlock
        ArgumentList = @($LogPath, $testMode, $combined)
        ErrorAction  = 'Continue'
    }
    if ($Credential) { $splat['Credential'] = $Credential }

    $results = @()

    foreach ($name in $ComputerNames) {
        $name = $name.Trim()
        if (-not $name) { continue }

        _sqmSplunkGuiLog "Verbinde zu $name ..." $LogCallback
        $entry = [PSCustomObject]@{ Computer = $name; Status = ''; Fehler = '' }

        if (-not (Test-Connection -ComputerName $name -Count 1 -Quiet)) {
            Write-Warning "$name nicht erreichbar - wird uebersprungen."
            $entry.Status = 'Nicht erreichbar'
            $results += $entry
            continue
        }

        try {
            Invoke-Command -ComputerName $name @splat
            $entry.Status = 'Erfolgreich'
            _sqmSplunkGuiLog " $name - OK" $LogCallback
        } catch {
            Write-Warning "Fehler bei $name : $_"
            $entry.Status = 'Fehler'
            $entry.Fehler = $_.Exception.Message
        }
        $results += $entry
    }

    _sqmSplunkGuiLog '' $LogCallback
    _sqmSplunkGuiLog '=== Zusammenfassung ===' $LogCallback
    $results | Format-Table -AutoSize
    return $results
}

# AD-OU-Modus
function _sqmSplunk_ForOU {
    param(
        [string]$SearchOU,
        [string]$Mode,
        [string]$LogPath,
        [System.Management.Automation.PSCredential]$Credential,
        [ScriptBlock]$LogCallback
    )

    Install-sqmAdModule

    $domainDN   = (Get-ADDomain).DistinguishedName
    $searchBase = if ($SearchOU -match '^OU=') { $SearchOU } else { "OU=$SearchOU,$domainDN" }

    _sqmSplunkGuiLog "AD-Suche unter: $searchBase" $LogCallback

    $adSplat = @{ Filter = '*'; SearchBase = $searchBase; Properties = 'OperatingSystem' }
    if ($Credential) { $adSplat['Credential'] = $Credential }

    $computers = Get-ADComputer @adSplat | Where-Object { $_.OperatingSystem -match 'Server' }

    if ($computers.Count -eq 0) {
        _sqmSplunkGuiLog "Keine Server in der OU '$SearchOU' gefunden." $LogCallback
        return
    }

    $names = $computers | Select-Object -ExpandProperty Name
    _sqmSplunkGuiLog "$($names.Count) Server in der OU gefunden." $LogCallback

    _sqmSplunk_OnComputers -ComputerNames $names -Mode $Mode `
                           -LogPath $LogPath -Credential $Credential -LogCallback $LogCallback
}

# Explizite Computerliste (Array oder Textdatei)
function _sqmSplunk_ForList {
    param(
        [string[]]$ComputerList,
        [string]$Mode,
        [string]$LogPath,
        [System.Management.Automation.PSCredential]$Credential,
        [ScriptBlock]$LogCallback
    )

    $resolved = @()

    foreach ($entry in $ComputerList) {
        $entry = $entry.Trim()
        if (-not $entry) { continue }

        if (Test-Path -LiteralPath $entry -PathType Leaf) {
            _sqmSplunkGuiLog "Lese Computernamen aus Datei: $entry" $LogCallback
            $lines = Get-Content -LiteralPath $entry -Encoding UTF8 |
                     Where-Object { $_ -and $_.Trim() -ne '' -and -not $_.TrimStart().StartsWith('#') }
            foreach ($line in $lines) {
                $n = $line.Trim()
                if ($n) { $resolved += $n }
            }
        } else {
            $resolved += $entry
        }
    }

    if ($resolved.Count -eq 0) {
        _sqmSplunkGuiLog 'Keine Computernamen in der Liste gefunden.' $LogCallback
        return
    }

    $unique = $resolved | Select-Object -Unique
    _sqmSplunkGuiLog "$($unique.Count) eindeutige Computer." $LogCallback

    _sqmSplunk_OnComputers -ComputerNames $unique -Mode $Mode `
                           -LogPath $LogPath -Credential $Credential -LogCallback $LogCallback
}


# ---------------------------------------------------------------------------
# Oeffentliche Funktion
# ---------------------------------------------------------------------------

function Invoke-sqmSplunkConfiguration {
    <#
    .SYNOPSIS
        Configures the Splunk Universal Forwarder on SQL Server hosts.
    .DESCRIPTION
        Detects all SQL Server instances, sets machine-wide environment variables
        for the ErrorLog path (MSSQL1_Log, MSSQL2_Log, ...) and manages the
        SplunkForwarder service — locally or remotely on any number of servers.
        Existing environment variables are not overwritten.
    .PARAMETER Mode
        Set - Set environment variables and start/restart SplunkForwarder (default).
        Test - Check only, no changes.
    .PARAMETER Remote
        Remote execution via AD OU search. Combine with -SearchOU.
    .PARAMETER SearchOU
        Distinguished Name or simple OU name. Default: OUServDatabase.
    .PARAMETER ComputerList
        Explicit server list: string array or path to a text file (# = comment).
    .PARAMETER Credential
        Credentials for AD and remoting.
    .PARAMETER LogPath
        Directory for log files. Default: sqmSQLTool LogPath configuration.
    .PARAMETER LogCallback
        Optional ScriptBlock for GUI logging.
    .EXAMPLE
        Invoke-sqmSplunkConfiguration
    .EXAMPLE
        Invoke-sqmSplunkConfiguration -Mode Test
    .EXAMPLE
        Invoke-sqmSplunkConfiguration -Remote -SearchOU "OU=DB-Server,DC=contoso,DC=com"
    .EXAMPLE
        Invoke-sqmSplunkConfiguration -ComputerList "SRV-SQL01","SRV-SQL02"
    .EXAMPLE
        Invoke-sqmSplunkConfiguration -ComputerList "C:\Listen\db-server.txt" -Mode Test
    .NOTES
        Set mode requires local administrator rights.
        Remote: WinRM must be active on target servers.
        AD OU mode: ActiveDirectory module is automatically installed if needed.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Local')]
    param(
        [ValidateSet('Set', 'Test')]
        [string]$Mode = 'Set',

        [Parameter(ParameterSetName = 'Remote')]
        [switch]$Remote,

        [Parameter(ParameterSetName = 'Remote')]
        [string]$SearchOU = 'OUServDatabase',

        [Parameter(ParameterSetName = 'List')]
        [string[]]$ComputerList,

        [Parameter(ParameterSetName = 'Remote')]
        [Parameter(ParameterSetName = 'List')]
        [System.Management.Automation.PSCredential]$Credential,

        [string]$LogPath,

        [ScriptBlock]$LogCallback
    )

    if (-not $LogPath) {
        $LogPath = Get-sqmConfig -Key 'LogPath'
        if (-not $LogPath) { $LogPath = '$env:ProgramData\sqmSQLTool\Logs' }
    }

    _sqmSplunkGuiLog "Invoke-sqmSplunkConfiguration | Modus: $Mode | ParameterSet: $($PSCmdlet.ParameterSetName)" $LogCallback

    if ($Remote) {
        _sqmSplunk_ForOU -SearchOU $SearchOU -Mode $Mode `
                         -LogPath $LogPath -Credential $Credential -LogCallback $LogCallback
    } elseif ($ComputerList) {
        _sqmSplunk_ForList -ComputerList $ComputerList -Mode $Mode `
                           -LogPath $LogPath -Credential $Credential -LogCallback $LogCallback
    } else {
        _sqmSplunk_LocalCore -LogPath $LogPath -TestMode ($Mode -eq 'Test')
    }
}