Public/New-sqmAgentProxy.ps1

<#
.SYNOPSIS
    Erstellt einen SQL Server Credential und einen SQL Agent Proxy und verbindet beide.

.DESCRIPTION
    Legt in einem Schritt einen neuen SQL Server Credential an und erstellt darauf
    basierend einen SQL Server Agent Proxy. Die Windows-Credentials werden interaktiv
    per Get-Credential abgefragt. Der Account wird vor der Erstellung auf Existenz
    und Eignung geprueft (Enabled, nicht gesperrt, Passwort nicht abgelaufen,
    Konto nicht abgelaufen). Ueber -Subsystem wird gesteuert welche Subsysteme
    dem Proxy zugewiesen werden.

    Ablauf:
      1. Get-Credential Dialog - Windows-Account eingeben
      2. AD-Pruefung: Existenz, Enabled, LockedOut, PasswordExpired, AccountExpired
      3. Pruefen ob Credential bereits existiert (Fehler oder -Force)
      4. Credential anlegen (CREATE CREDENTIAL) via SMO
      5. Pruefen ob Proxy bereits existiert (Fehler oder -Force)
      6. Agent Proxy anlegen und mit dem Credential verbinden via SMO
      7. Subsysteme gemaess -Subsystem zuweisen (CmdExec, SSIS, PowerShell oder All)
      8. Protokoll-Objekt zurueckgeben

.PARAMETER SqlInstance
    SQL Server-Instanz. Standard: lokaler Computername.

.PARAMETER SqlCredential
    PSCredential fuer die SQL-Verbindung (Windows-Auth wenn nicht angegeben).

.PARAMETER CredentialName
    Name des neuen SQL Server Credentials (z.B. "DOMAIN\ServiceAccount").

.PARAMETER ProxyName
    Name des neuen SQL Agent Proxys.

.PARAMETER ProxyDescription
    Optionale Beschreibung fuer den Proxy.

.PARAMETER WindowsCredential
    Windows-Credential direkt als PSCredential uebergeben (kein Dialog).
    Wenn nicht angegeben erscheint ein Get-Credential Dialog.
    Kann z.B. aus einem Passwort-Safe oder vorherigem Get-Credential stammen.

.PARAMETER WindowsUserName
    Optionaler Windows-Benutzername (DOMAIN\User) zur Vorbestueckung des
    Get-Credential Dialogs. Wird ignoriert wenn -WindowsCredential angegeben.
    Wenn nicht angegeben wird der CredentialName als Vorschlag verwendet.

.PARAMETER Subsystem
    Subsysteme die dem Proxy zugewiesen werden. Mehrfachauswahl moeglich.
    Gueltiger Werte: CmdExec, SSIS, PowerShell, All
    Standard: All (alle drei Subsysteme)

.PARAMETER Force
    Ueberschreibt bestehenden Credential und/oder Proxy wenn vorhanden.

.PARAMETER EnableException
    Ausnahmen sofort ausloesen statt Write-Error.

.EXAMPLE
    # Einzeiler - Credential-Dialog erscheint automatisch
    New-sqmAgentProxy -SqlInstance "SQL01" -CredentialName "DOMAIN\SqlServiceAccount" `
        -ProxyName "SSIS Proxy"

    # Credential direkt uebergeben - kein Dialog
    $cred = Get-Credential "DOMAIN\SvcSSIS"
    New-sqmAgentProxy -SqlInstance "SQL01" -CredentialName "DOMAIN\SvcSSIS" `
        -ProxyName "SSIS Proxy" -WindowsCredential $cred

.EXAMPLE
    # Nur SSIS - Benutzername vorausgewaehlt im Dialog
    New-sqmAgentProxy -SqlInstance "SQL01" -CredentialName "DOMAIN\SvcSSIS" `
        -ProxyName "SSIS Only Proxy" -Subsystem SSIS

.EXAMPLE
    # CmdExec und PowerShell
    New-sqmAgentProxy -SqlInstance "SQL01" -CredentialName "DOMAIN\SvcPS" `
        -ProxyName "Script Proxy" -Subsystem CmdExec, PowerShell

.EXAMPLE
    # Abweichender Windows-Account und Force
    New-sqmAgentProxy -SqlInstance "SQL01" -CredentialName "ProxyCred_SSIS" `
        -ProxyName "SSIS Proxy" -WindowsUserName "DOMAIN\SvcSSIS" -Force

.EXAMPLE
    # Unattended / Skript-Betrieb mit SecureString aus Vault
    $secPwd = ConvertTo-SecureString "P@ssw0rd" -AsPlainText -Force
    $cred = New-Object System.Management.Automation.PSCredential("DOMAIN\SvcSSIS", $secPwd)
    New-sqmAgentProxy -SqlInstance "SQL01" -CredentialName "DOMAIN\SvcSSIS" `
        -ProxyName "SSIS Proxy" -WindowsCredential $cred -Subsystem SSIS
    New-sqmAgentProxy -SqlInstance "SQL01\INST1" -CredentialName "DOMAIN\SvcSSIS" `
        -ProxyName "SSIS Execution Proxy" -ProxyDescription "Fuehrt SSIS-Pakete aus" `
        -WindowsCredential $winCred -Force

.NOTES
    Erfordert: SMO (Microsoft.SqlServer.Smo), Invoke-sqmLogging
    Benoetigt: sysadmin auf der Instanz
    Subsysteme: CmdExec (1), SSIS (11), PowerShell (12)
#>

function New-sqmAgentProxy
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string]$SqlInstance,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$SqlCredential,

        [Parameter(Mandatory = $true)]
        [string]$CredentialName,

        [Parameter(Mandatory = $true)]
        [string]$ProxyName,

        [Parameter(Mandatory = $false)]
        [string]$ProxyDescription = '',

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$WindowsCredential,

        [Parameter(Mandatory = $false)]
        [string]$WindowsUserName,

        [Parameter(Mandatory = $false)]
        [ValidateSet('CmdExec', 'SSIS', 'PowerShell', 'All')]
        [string[]]$Subsystem = @('All'),

        [Parameter(Mandatory = $false)]
        [switch]$Force,

        [Parameter(Mandatory = $false)]
        [switch]$EnableException
    )

    begin
    {
        $functionName = $MyInvocation.MyCommand.Name

        if (-not $PSBoundParameters.ContainsKey('SqlInstance') -or [string]::IsNullOrWhiteSpace($SqlInstance))
        {
            $SqlInstance = $env:COMPUTERNAME
        }

        # SMO Assembly laden
        try
        {
            [void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.Smo')
            [void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SqlEnum')
        }
        catch
        {
            $msg = "SMO konnte nicht geladen werden: $($_.Exception.Message)"
            Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level 'ERROR'
            if ($EnableException) { throw $msg }
            Write-Error $msg
            return
        }

        Invoke-sqmLogging -Message "Starte $functionName auf $SqlInstance" -FunctionName $functionName -Level 'INFO'
    }

    process
    {
        try
        {
            # ---------------------------------------------------------------
            # 1. Windows-Credential ermitteln (Parameter oder Dialog)
            # ---------------------------------------------------------------
            if ($WindowsCredential)
            {
                Write-Host ""
                Write-Host " Windows-Credential direkt uebernommen: $($WindowsCredential.UserName)" -ForegroundColor Cyan
                Invoke-sqmLogging -Message "WindowsCredential per Parameter: $($WindowsCredential.UserName)" -FunctionName $functionName -Level 'INFO'
            }
            else
            {
                $dialogUser = if ($WindowsUserName) { $WindowsUserName } else { $CredentialName }

                Write-Host ""
                Write-Host " Windows-Account fuer Proxy '$ProxyName'" -ForegroundColor Cyan
                Write-Host " Bitte Credentials eingeben ..." -ForegroundColor Gray

                $WindowsCredential = Get-Credential -UserName $dialogUser `
                    -Message "Windows-Account fuer SQL Agent Proxy '$ProxyName' auf $SqlInstance"

                if (-not $WindowsCredential)
                {
                    throw "Abgebrochen - kein Credential eingegeben."
                }
            }

            # ---------------------------------------------------------------
            # 2. AD-Konto pruefen
            # ---------------------------------------------------------------
            # SamAccountName aus DOMAIN\User oder user@domain extrahieren
            $rawUser = $WindowsCredential.UserName
            $samAccountName = if ($rawUser -match '\\')
            {
                $rawUser.Split('\')[1]
            }
            elseif ($rawUser -match '@')
            {
                $rawUser.Split('@')[0]
            }
            else
            {
                $rawUser
            }

            Write-Host " Pruefe AD-Konto '$samAccountName' ..." -ForegroundColor Gray
            Invoke-sqmLogging -Message "AD-Pruefung fuer Konto '$samAccountName'." -FunctionName $functionName -Level 'INFO'

            $adStatus = Get-sqmADAccountStatus -SamAccountName $samAccountName -ErrorAction SilentlyContinue

            if (-not $adStatus -or $adStatus.ErrorMessage)
            {
                throw "AD-Konto '$samAccountName' nicht gefunden oder nicht erreichbar: $($adStatus.ErrorMessage)"
            }

            # Eignung pruefen
            $issues = [System.Collections.Generic.List[string]]::new()
            if (-not $adStatus.Enabled)       { $issues.Add("Konto ist deaktiviert") }
            if ($adStatus.LockedOut)          { $issues.Add("Konto ist gesperrt") }
            if ($adStatus.PasswordExpired)    { $issues.Add("Passwort abgelaufen") }
            if ($adStatus.AccountExpired)     { $issues.Add("Konto abgelaufen") }

            if ($issues.Count -gt 0)
            {
                $issueList = $issues -join ', '
                throw "AD-Konto '$samAccountName' ist nicht geeignet: $issueList"
            }

            Write-Host " AD-Konto OK: $rawUser (Enabled, nicht gesperrt)" -ForegroundColor Green
            Invoke-sqmLogging -Message "AD-Konto '$samAccountName' geeignet (Source: $($adStatus.Source))." -FunctionName $functionName -Level 'INFO'

            # ---------------------------------------------------------------
            # 3. SMO-Verbindung aufbauen
            # ---------------------------------------------------------------
            $serverConn = New-Object Microsoft.SqlServer.Management.Smo.Server($SqlInstance)

            if ($SqlCredential)
            {
                $serverConn.ConnectionContext.LoginSecure = $false
                $serverConn.ConnectionContext.Login       = $SqlCredential.UserName
                $serverConn.ConnectionContext.SecurePassword = $SqlCredential.Password
            }

            # Verbindung testen
            $null = $serverConn.Databases.Count
            Invoke-sqmLogging -Message "SMO-Verbindung zu $SqlInstance hergestellt (Version: $($serverConn.VersionString))." -FunctionName $functionName -Level 'INFO'

            # ---------------------------------------------------------------
            # 4. Credential anlegen
            # ---------------------------------------------------------------
            $existingCred = $serverConn.Credentials | Where-Object { $_.Name -eq $CredentialName }

            if ($existingCred)
            {
                if ($Force)
                {
                    Invoke-sqmLogging -Message "Credential '$CredentialName' existiert bereits - wird mit -Force geloescht und neu angelegt." -FunctionName $functionName -Level 'WARNING'
                    $existingCred.Drop()
                }
                else
                {
                    throw "Credential '$CredentialName' existiert bereits auf '$SqlInstance'. Verwende -Force zum Ueberschreiben."
                }
            }

            if (-not $PSCmdlet.ShouldProcess($SqlInstance, "Credential '$CredentialName' anlegen"))
            {
                return $null
            }

            $credential = New-Object Microsoft.SqlServer.Management.Smo.Credential($serverConn, $CredentialName)
            $credential.Identity = $WindowsCredential.UserName

            # Passwort als SecureString uebergeben
            $credential.Create($WindowsCredential.Password)

            Invoke-sqmLogging -Message "Credential '$CredentialName' (Identity: $($WindowsCredential.UserName)) erfolgreich angelegt." -FunctionName $functionName -Level 'INFO'

            # ---------------------------------------------------------------
            # 5. Agent Proxy anlegen
            # ---------------------------------------------------------------
            $jobServer      = $serverConn.JobServer
            $existingProxy  = $jobServer.ProxyAccounts | Where-Object { $_.Name -eq $ProxyName }

            if ($existingProxy)
            {
                if ($Force)
                {
                    Invoke-sqmLogging -Message "Proxy '$ProxyName' existiert bereits - wird mit -Force geloescht." -FunctionName $functionName -Level 'WARNING'
                    $existingProxy.Drop()
                }
                else
                {
                    throw "Proxy '$ProxyName' existiert bereits auf '$SqlInstance'. Verwende -Force zum Ueberschreiben."
                }
            }

            if (-not $PSCmdlet.ShouldProcess($SqlInstance, "Agent Proxy '$ProxyName' anlegen"))
            {
                return $null
            }

            $proxy = New-Object Microsoft.SqlServer.Management.Smo.Agent.ProxyAccount(
                $jobServer,
                $ProxyName,
                $CredentialName,
                $true,   # IsEnabled
                $ProxyDescription
            )

            $proxy.Create()
            Invoke-sqmLogging -Message "Agent Proxy '$ProxyName' mit Credential '$CredentialName' angelegt." -FunctionName $functionName -Level 'INFO'

            # ---------------------------------------------------------------
            # 6. Subsysteme zuweisen
            # ---------------------------------------------------------------
            # AgentSubSystem Enum-Werte:
            # CmdExec = 1
            # Ssis = 11
            # PowerShell = 12

            # Mapping: Parameterwert -> SMO Enum
            $subsystemMap = @{
                'CmdExec'    = [Microsoft.SqlServer.Management.Smo.Agent.AgentSubSystem]::CmdExec
                'SSIS'       = [Microsoft.SqlServer.Management.Smo.Agent.AgentSubSystem]::Ssis
                'PowerShell' = [Microsoft.SqlServer.Management.Smo.Agent.AgentSubSystem]::PowerShell
            }

            # 'All' aufloesen
            $resolved = if ($Subsystem -contains 'All')
            {
                $subsystemMap.Values
            }
            else
            {
                $Subsystem | ForEach-Object { $subsystemMap[$_] }
            }

            $assignedSubsystems = [System.Collections.Generic.List[string]]::new()

            foreach ($sub in $resolved)
            {
                $proxy.AddSubSystem($sub)
                $assignedSubsystems.Add($sub.ToString())
                Invoke-sqmLogging -Message "Subsystem '$sub' dem Proxy '$ProxyName' zugewiesen." -FunctionName $functionName -Level 'INFO'
            }

            # ---------------------------------------------------------------
            # 7. Ergebnis
            # ---------------------------------------------------------------
            $result = [PSCustomObject]@{
                SqlInstance          = $SqlInstance
                CredentialName       = $CredentialName
                CredentialIdentity   = $WindowsCredential.UserName
                ADAccountVerified    = $true
                ADAccountSource      = $adStatus.Source
                ProxyName            = $ProxyName
                ProxyDescription     = $ProxyDescription
                AssignedSubsystems   = $assignedSubsystems -join ', '
                IsEnabled            = $true
                Success              = $true
            }

            Write-Host ""
            Write-Host " Agent Proxy erfolgreich erstellt" -ForegroundColor Green
            Write-Host " --------------------------------" -ForegroundColor DarkGray
            Write-Host " Instanz : $SqlInstance"       -ForegroundColor Cyan
            Write-Host " Credential : $CredentialName"    -ForegroundColor Cyan
            Write-Host " Identity : $($WindowsCredential.UserName)" -ForegroundColor Cyan
            Write-Host " Proxy : $ProxyName"         -ForegroundColor Cyan
            Write-Host " Subsysteme : $($assignedSubsystems -join ', ')" -ForegroundColor Cyan
            Write-Host ""

            return $result
        }
        catch
        {
            $errMsg = "Fehler in $functionName`: $($_.Exception.Message)"
            Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level 'ERROR'
            if ($EnableException) { throw }
            Write-Error $errMsg
            return [PSCustomObject]@{ Success = $false; ErrorMessage = $errMsg }
        }
    }

    end
    {
        Invoke-sqmLogging -Message "$functionName abgeschlossen." -FunctionName $functionName -Level 'INFO'
    }
}