Public/Install-sqmSsrsReportServer.ps1

<#
.SYNOPSIS
    Installs SQL Server Reporting Services 2022 from a network share
    and automatically configures the instance afterwards.

.DESCRIPTION
    Executes the following steps in sequence:

    [1] Check prerequisites
        - Administrator rights on the target computer
        - Installer (.exe or .msi) found in the configured share (SsrsInstallerPath)
        - SSRS not yet installed (skippable with -Force)

    [2] Installation
        - Copies the installer to a local temp directory (UNC paths are not
          directly supported as process start)
        - Runs the installer silently:
            SQLServerReportingServices.exe
                /quiet /IAcceptLicenseTerms /Edition=<Edition> /IAcceptLicenseTerms
        - Evaluates the exit code (0 = OK, 3010 = restart recommended)
        - Waits up to 60 seconds for the SSRS WMI namespace (service startup)

    [3] Configuration
        - Calls Set-sqmSsrsConfiguration with all passed configuration parameters
          (splatting). Parameters not passed use the defaults of Set-sqmSsrsConfiguration.

    The installer path is read preferably from the -InstallerPath parameter.
    If missing, Get-sqmConfig -Key 'SsrsInstallerPath' is used.
    If that is also not set, an error is thrown.

.PARAMETER ComputerName
    Target computer. Default: $env:COMPUTERNAME (local).
    Remote installation via WinRM / PsRemoting is supported.

.PARAMETER InstallerPath
    Full UNC or local path to the installation file
    (SQLServerReportingServices.exe or .msi).
    Overrides Get-sqmConfig -Key 'SsrsInstallerPath'.

.PARAMETER Edition
    License edition for the silent parameter /Edition.
    Valid values: Eval, Developer, Expr, Web, Standard, Enterprise.
    Default: 'Developer'.

.PARAMETER ProductKey
    Product key (25 characters). If specified, instead of -Edition the
    parameter /IAcceptLicenseTerms /PID:<Key> is used.

.PARAMETER Force
    Perform installation even if SSRS is already installed.

.PARAMETER SkipConfiguration
    Install only; do not call Set-sqmSsrsConfiguration.

.PARAMETER InstanceName
    SSRS instance name. Passed to Set-sqmSsrsConfiguration.
    Default: 'MSSQLSERVER'.

.PARAMETER DatabaseServer
    SQL Server for the ReportServer database.
    Passed to Set-sqmSsrsConfiguration.

.PARAMETER DatabaseName
    Name of the ReportServer database. Default: 'ReportServer'.

.PARAMETER ReportServerUrl
    URL for the ReportServer web service.
    Default: 'http://+:80/ReportServer'.

.PARAMETER ReportsUrl
    URL for the reports portal. Default: 'http://+:80/Reports'.

.PARAMETER ServiceAccount
    Windows service account for SSRS.

.PARAMETER ServiceAccountPassword
    Password for -ServiceAccount (SecureString).

.PARAMETER DatabaseAuthType
    Authentication for the DB connection: 'Windows' or 'SQL'.

.PARAMETER DatabaseCredential
    PSCredential for SQL authentication (only with -DatabaseAuthType SQL).

.PARAMETER EncryptionKeyFile
    Path for the encryption key backup (.snk).

.PARAMETER EncryptionKeyPassword
    Password for the key backup (SecureString).

.PARAMETER SkipDatabase
    Skip database configuration in Set-sqmSsrsConfiguration.

.PARAMETER SkipUrls
    Skip URL configuration in Set-sqmSsrsConfiguration.

.PARAMETER SkipServiceAccount
    Skip service account configuration in Set-sqmSsrsConfiguration.

.PARAMETER SkipEncryptionKeyBackup
    Skip the key backup in Set-sqmSsrsConfiguration.

.PARAMETER Credential
    PSCredential for the WinRM connection to the target computer (remote operation).

.PARAMETER OutputPath
    Output directory for the configuration report.

.PARAMETER WmiWaitSeconds
    Maximum wait time in seconds for the SSRS WMI namespace after installation.
    Default: 60.

.PARAMETER ContinueOnError
    Do not treat configuration errors as terminating.

.PARAMETER EnableException
    Throw exceptions immediately.

.OUTPUTS
    [PSCustomObject] with the following fields:
        ComputerName, InstallerUsed, Edition, InstallExitCode,
        RebootRequired, InstallResult, ConfigResult, OverallStatus, Message

.EXAMPLE
    Install-sqmSsrsReportServer

    Installs SSRS using the installer path stored in sqmConfig,
    Edition Developer, followed by full configuration with default values.

.EXAMPLE
    Install-sqmSsrsReportServer `
        -InstallerPath '\\srv-share\Software\SSRS2022\SQLServerReportingServices.exe' `
        -Edition Standard `
        -DatabaseServer 'SQL-AG-Listener' `
        -ServiceAccount 'DOMAIN\svc_ssrs' `
        -EncryptionKeyPassword (Read-Host -AsSecureString 'Key-Passwort')

.EXAMPLE
    Install-sqmSsrsReportServer -SkipConfiguration -WhatIf

    Shows what would be installed without making any changes.

.NOTES
    Prerequisites : Invoke-sqmLogging, Set-sqmSsrsConfiguration, local administrator rights
    Supported versions: SSRS 2022 (SQLServerReportingServices.exe)
    Exit Codes : 0 = Success, 3010 = Restart recommended, other = Error
    WMI Namespace : root\Microsoft\SqlServer\ReportServer
#>

function Install-sqmSsrsReportServer
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    [OutputType([PSCustomObject])]
    param (
        # ?? Zielrechner ????????????????????????????????????????????????????
        [Parameter(Mandatory = $false)]
        [string]$ComputerName = $env:COMPUTERNAME,

        # ?? Installer-Quelle ???????????????????????????????????????????????
        [Parameter(Mandatory = $false)]
        [string]$InstallerPath,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Eval', 'Developer', 'Expr', 'Web', 'Standard', 'Enterprise')]
        [string]$Edition = 'Developer',

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

        # ?? Installations-Steuerung ????????????????????????????????????????
        [Parameter(Mandatory = $false)]
        [switch]$Force,

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

        [Parameter(Mandatory = $false)]
        [int]$WmiWaitSeconds = 60,

        # ?? Konfigurations-Parameter (Durchreich an Set-sqmSsrsConfiguration) ??
        [Parameter(Mandatory = $false)]
        [string]$InstanceName = 'MSSQLSERVER',

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

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

        [Parameter(Mandatory = $false)]
        [string]$ReportServerUrl = 'http://+:80/ReportServer',

        [Parameter(Mandatory = $false)]
        [string]$ReportsUrl = 'http://+:80/Reports',

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

        [Parameter(Mandatory = $false)]
        [System.Security.SecureString]$ServiceAccountPassword,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Windows', 'SQL')]
        [string]$DatabaseAuthType = 'Windows',

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

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

        [Parameter(Mandatory = $false)]
        [System.Security.SecureString]$EncryptionKeyPassword,

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

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

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

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

        # ?? Verbindung / Ausgabe ???????????????????????????????????????????
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$Credential,

        [Parameter(Mandatory = $false)]
        [string]$OutputPath = (Get-sqmConfig -Key 'OutputPath'),

        # ?? Fehlerbehandlung ???????????????????????????????????????????????
        [Parameter(Mandatory = $false)]
        [switch]$ContinueOnError,

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

    begin
    {
        $functionName = $MyInvocation.MyCommand.Name
        $isLocal      = $ComputerName -in @($env:COMPUTERNAME, 'localhost', '127.0.0.1', '.')

        # ?? Rueckgabe-Objekt vorab befuellen ?????????????????????????????????
        $result = [PSCustomObject]@{
            ComputerName     = $ComputerName
            InstallerUsed    = $null
            Edition          = $Edition
            InstallExitCode  = $null
            RebootRequired   = $false
            InstallResult    = 'NotStarted'
            ConfigResult     = 'Skipped'
            OverallStatus    = 'Unknown'
            Message          = $null
        }

        # ?? Hilfsfunktion: einheitliches Fehlerhandling ???????????????????
        function _Fail ([string]$msg)
        {
            Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level 'ERROR'
            $result.OverallStatus = 'Failed'
            $result.Message       = $msg
            if ($EnableException) { throw $msg }
            Write-Error $msg
            return $result
        }

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

    process
    {
        # ??????????????????????????????????????????????????????????????????
        # [1] Installer-Pfad aufloesen
        # ??????????????????????????????????????????????????????????????????
        $effectiveInstaller = $InstallerPath
        if (-not $effectiveInstaller)
        {
            $effectiveInstaller = Get-sqmConfig -Key 'SsrsInstallerPath'
        }

        if ([string]::IsNullOrWhiteSpace($effectiveInstaller))
        {
            return (_Fail ("Kein Installer-Pfad angegeben und 'SsrsInstallerPath' ist nicht in der " +
                           "Modulkonfiguration gesetzt. Bitte -InstallerPath angeben oder " +
                           "Set-sqmConfig -SsrsInstallerPath '<UNC-Pfad>' ausfuehren."))
        }

        $installerExtension = [System.IO.Path]::GetExtension($effectiveInstaller).ToLower()
        if ($installerExtension -notin @('.exe', '.msi'))
        {
            return (_Fail "Installer-Datei '$effectiveInstaller' hat keine unterstuetzte Erweiterung (.exe / .msi).")
        }

        if (-not (Test-Path -LiteralPath $effectiveInstaller))
        {
            return (_Fail "Installer-Datei nicht erreichbar: '$effectiveInstaller'")
        }

        $result.InstallerUsed = $effectiveInstaller
        Invoke-sqmLogging -Message "Installer: '$effectiveInstaller'" `
                          -FunctionName $functionName -Level 'INFO'

        # ??????????????????????????????????????????????????????????????????
        # [2] Voraussetzungen pruefen
        # ??????????????????????????????????????????????????????????????????

        # 2a. Administratorrechte (nur bei lokaler Ausfuehrung direkt pruefbar)
        if ($isLocal)
        {
            $principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
            if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
            {
                return (_Fail "Keine lokalen Administratorrechte - Installation nicht moeglich. PowerShell als Administrator starten.")
            }
        }

        # 2b. Ist SSRS bereits installiert?
        $ssrsAlreadyInstalled = $false
        try
        {
            $cimParams = @{ Namespace = 'root\Microsoft\SqlServer\ReportServer'; ErrorAction = 'Stop' }
            if (-not $isLocal)
            {
                $sessionOpts         = New-CimSessionOption -Protocol Wsman
                $cimConnParams       = @{ ComputerName = $ComputerName; SessionOption = $sessionOpts }
                if ($Credential) { $cimConnParams['Credential'] = $Credential }
                $checkSession        = New-CimSession @cimConnParams -ErrorAction Stop
                $cimParams['CimSession'] = $checkSession
            }
            $nsCheck = Get-CimInstance @cimParams -ClassName '__NAMESPACE' -ErrorAction SilentlyContinue
            $ssrsAlreadyInstalled = $null -ne $nsCheck
            if (isset $checkSession) { Remove-CimSession $checkSession -ErrorAction SilentlyContinue }
        }
        catch
        {
            # Namespace nicht vorhanden = SSRS nicht installiert, das ist der Normalfall
            $ssrsAlreadyInstalled = $false
        }

        if ($ssrsAlreadyInstalled -and -not $Force)
        {
            $msg = "SSRS ist auf '$ComputerName' bereits installiert. " +
                   "Verwenden Sie -Force um die Installation trotzdem durchzufuehren."
            Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level 'WARNING'
            Write-Warning $msg
            $result.InstallResult = 'AlreadyInstalled'
            $result.OverallStatus = 'AlreadyInstalled'
            $result.Message       = $msg

            # Wenn SSRS schon da und SkipConfiguration nicht gesetzt:
            # direkt zur Konfiguration springen
            if (-not $SkipConfiguration)
            {
                Invoke-sqmLogging -Message "ueberspringe Installation - fahre direkt mit Konfiguration fort." `
                                  -FunctionName $functionName -Level 'INFO'
                # Goto Konfigurations-Block (ueber $skipInstall-Flag)
            }
            else
            {
                return $result
            }
        }

        # ??????????????????????????????????????????????????????????????????
        # [3] Installation
        # ??????????????????????????????????????????????????????????????????
        $skipInstall = ($ssrsAlreadyInstalled -and -not $Force)

        if (-not $skipInstall)
        {
            if ($PSCmdlet.ShouldProcess($ComputerName, "SSRS 2022 installieren von '$effectiveInstaller'"))
            {
                try
                {
                    # Installer in lokales Temp kopieren
                    # (UNC-Pfade koennen nicht direkt als Prozess gestartet werden)
                    $tempDir       = Join-Path $env:TEMP "SsrsInstall_$(Get-Random)"
                    $tempInstaller = Join-Path $tempDir ([System.IO.Path]::GetFileName($effectiveInstaller))
                    New-Item -ItemType Directory -Path $tempDir -Force | Out-Null

                    Invoke-sqmLogging -Message "Kopiere Installer nach '$tempInstaller'..." `
                                      -FunctionName $functionName -Level 'INFO'
                    Write-Host " [1/2] Installer kopieren von Share..." -ForegroundColor Gray
                    Copy-Item -LiteralPath $effectiveInstaller -Destination $tempInstaller -Force -ErrorAction Stop

                    # Silent-Parameter zusammenstellen
                    # SSRS 2022 EXE: /quiet /IAcceptLicenseTerms /Edition=<X>
                    # SSRS 2022 MSI: /quiet /norestart IACCEPTLICENSETERMS=YES
                    $isMsi = $installerExtension -eq '.msi'

                    if ($isMsi)
                    {
                        $installArgs = @(
                            '/i', "`"$tempInstaller`"",
                            '/quiet',
                            '/norestart',
                            'IACCEPTLICENSETERMS=YES'
                        )
                        if ($ProductKey)
                        {
                            $installArgs += "PIDKEY=$ProductKey"
                        }
                        else
                        {
                            $installArgs += "EDITION=$Edition"
                        }
                        $installExe = 'msiexec.exe'
                    }
                    else
                    {
                        $installArgs = @(
                            '/quiet',
                            '/IAcceptLicenseTerms',
                            '/norestart'
                        )
                        if ($ProductKey)
                        {
                            $installArgs += "/PID=$ProductKey"
                        }
                        else
                        {
                            $installArgs += "/Edition=$Edition"
                        }
                        $installExe = $tempInstaller
                    }

                    $argString = $installArgs -join ' '
                    Invoke-sqmLogging -Message "Starte Installation: $installExe $argString" `
                                      -FunctionName $functionName -Level 'INFO'
                    Write-Host " [2/2] SSRS installieren (silent)..." -ForegroundColor Gray

                    $proc = Start-Process -FilePath $installExe `
                                         -ArgumentList $installArgs `
                                         -Wait -PassThru -NoNewWindow `
                                         -ErrorAction Stop

                    $result.InstallExitCode = $proc.ExitCode

                    switch ($proc.ExitCode)
                    {
                        0
                        {
                            $result.InstallResult  = 'OK'
                            $result.RebootRequired = $false
                            Write-Host " ? SSRS installiert (ExitCode 0)." -ForegroundColor Green
                            Invoke-sqmLogging -Message "Installation erfolgreich (ExitCode 0)." `
                                              -FunctionName $functionName -Level 'INFO'
                        }
                        3010
                        {
                            $result.InstallResult  = 'OK'
                            $result.RebootRequired = $true
                            Write-Host " ? SSRS installiert (ExitCode 3010 - Neustart empfohlen)." -ForegroundColor Yellow
                            Write-Warning "SSRS wurde installiert, ein Neustart wird empfohlen."
                            Invoke-sqmLogging -Message "Installation erfolgreich, Neustart empfohlen (ExitCode 3010)." `
                                              -FunctionName $functionName -Level 'WARNING'
                        }
                        default
                        {
                            throw "Installer beendet mit ExitCode $($proc.ExitCode). Installation fehlgeschlagen."
                        }
                    }
                }
                catch
                {
                    $errMsg = "Installations-Fehler: $($_.Exception.Message)"
                    Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level 'ERROR'
                    $result.InstallResult = 'Failed'
                    $result.OverallStatus = 'Failed'
                    $result.Message       = $errMsg
                    if ($EnableException) { throw }
                    Write-Error $errMsg
                    return $result
                }
                finally
                {
                    if (Test-Path $tempDir)
                    {
                        Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
                    }
                }

                # ?? WMI-Namespace abwarten ????????????????????????????????
                # SSRS-Dienst braucht nach dem Installer-Start einige Sekunden
                # bis der WMI-Provider registriert ist.
                Invoke-sqmLogging -Message "Warte auf SSRS-WMI-Namespace (max. $WmiWaitSeconds s)..." `
                                  -FunctionName $functionName -Level 'INFO'
                Write-Host " Warte auf SSRS-WMI-Namespace..." -ForegroundColor Gray

                $wmiReady    = $false
                $waitStart   = [datetime]::UtcNow
                $wmiNs       = 'root\Microsoft\SqlServer\ReportServer'
                $wmiPollBase = @{ Namespace = $wmiNs; ClassName = '__NAMESPACE'; ErrorAction = 'SilentlyContinue' }
                if (-not $isLocal -and $checkSession)
                {
                    $wmiPollBase['CimSession'] = $checkSession
                }

                while (-not $wmiReady -and ([datetime]::UtcNow - $waitStart).TotalSeconds -lt $WmiWaitSeconds)
                {
                    Start-Sleep -Seconds 3
                    try
                    {
                        $ns      = Get-CimInstance @wmiPollBase
                        $wmiReady = $null -ne $ns
                    }
                    catch { $wmiReady = $false }
                }

                if (-not $wmiReady)
                {
                    $warnMsg = "SSRS-WMI-Namespace nach $WmiWaitSeconds s noch nicht vorhanden. " +
                               "Konfiguration wird trotzdem versucht."
                    Invoke-sqmLogging -Message $warnMsg -FunctionName $functionName -Level 'WARNING'
                    Write-Warning $warnMsg
                }
                else
                {
                    Invoke-sqmLogging -Message "SSRS-WMI-Namespace verfuegbar nach $([int]([datetime]::UtcNow - $waitStart).TotalSeconds) s." `
                                      -FunctionName $functionName -Level 'INFO'
                    Write-Host " ? SSRS-WMI-Namespace bereit." -ForegroundColor Green
                }
            }
            else
            {
                # WhatIf-Pfad
                $result.InstallResult = 'WhatIf'
                Invoke-sqmLogging -Message "WhatIf: Installation wuerde gestartet werden." `
                                  -FunctionName $functionName -Level 'INFO'
            }
        }

        # ??????????????????????????????????????????????????????????????????
        # [4] Konfiguration via Set-sqmSsrsConfiguration
        # ??????????????????????????????????????????????????????????????????
        if ($SkipConfiguration)
        {
            $result.ConfigResult  = 'Skipped'
            $result.OverallStatus = $result.InstallResult
            $result.Message       = "Installation: $($result.InstallResult) | Konfiguration: uebersprungen (-SkipConfiguration)."
            Invoke-sqmLogging -Message $result.Message -FunctionName $functionName -Level 'INFO'
            return $result
        }

        if ($result.InstallResult -eq 'WhatIf')
        {
            $result.ConfigResult  = 'WhatIf'
            $result.OverallStatus = 'WhatIf'
            $result.Message       = 'WhatIf: Installation und Konfiguration wuerden durchgefuehrt.'
            return $result
        }

        Write-Host ""
        Write-Host "[$ComputerName] Starte SSRS-Konfiguration..." -ForegroundColor Cyan
        Invoke-sqmLogging -Message "Starte Set-sqmSsrsConfiguration auf '$ComputerName'." `
                          -FunctionName $functionName -Level 'INFO'

        # Nur explizit uebergebene Parameter weiterreichen (kein ueberschreiben der
        # Standardwerte in Set-sqmSsrsConfiguration durch leere Werte)
        $configSplat = @{
            ComputerName   = $ComputerName
            InstanceName   = $InstanceName
            DatabaseName   = $DatabaseName
            ReportServerUrl = $ReportServerUrl
            ReportsUrl     = $ReportsUrl
            DatabaseAuthType = $DatabaseAuthType
        }

        if ($PSBoundParameters.ContainsKey('DatabaseServer')          -and $DatabaseServer)          { $configSplat['DatabaseServer']          = $DatabaseServer }
        if ($PSBoundParameters.ContainsKey('ServiceAccount')          -and $ServiceAccount)          { $configSplat['ServiceAccount']          = $ServiceAccount }
        if ($PSBoundParameters.ContainsKey('ServiceAccountPassword')  -and $ServiceAccountPassword)  { $configSplat['ServiceAccountPassword']  = $ServiceAccountPassword }
        if ($PSBoundParameters.ContainsKey('DatabaseCredential')      -and $DatabaseCredential)      { $configSplat['DatabaseCredential']      = $DatabaseCredential }
        if ($PSBoundParameters.ContainsKey('EncryptionKeyFile')       -and $EncryptionKeyFile)       { $configSplat['EncryptionKeyFile']       = $EncryptionKeyFile }
        if ($PSBoundParameters.ContainsKey('EncryptionKeyPassword')   -and $EncryptionKeyPassword)   { $configSplat['EncryptionKeyPassword']   = $EncryptionKeyPassword }
        if ($PSBoundParameters.ContainsKey('Credential')              -and $Credential)              { $configSplat['Credential']              = $Credential }
        if ($PSBoundParameters.ContainsKey('OutputPath')              -and $OutputPath)              { $configSplat['OutputPath']              = $OutputPath }

        # Skip-Schalter
        if ($SkipDatabase)            { $configSplat['SkipDatabase']            = $true }
        if ($SkipUrls)                { $configSplat['SkipUrls']                = $true }
        if ($SkipServiceAccount)      { $configSplat['SkipServiceAccount']      = $true }
        if ($SkipEncryptionKeyBackup) { $configSplat['SkipEncryptionKeyBackup'] = $true }
        if ($ContinueOnError)         { $configSplat['ContinueOnError']         = $true }
        if ($EnableException)         { $configSplat['EnableException']         = $true }

        try
        {
            $configResult = Set-sqmSsrsConfiguration @configSplat
            $result.ConfigResult = $configResult.OverallStatus

            $result.OverallStatus = switch ($true)
            {
                ($result.InstallResult -in @('OK', 'AlreadyInstalled') -and
                 $configResult.OverallStatus -eq 'Success')            { 'Success' }
                ($configResult.OverallStatus -eq 'PartialSuccess')     { 'PartialSuccess' }
                default                                                 { 'Failed' }
            }

            $result.Message = "Installation: $($result.InstallResult) | " +
                              "Konfiguration: $($configResult.OverallStatus) | " +
                              $configResult.Message
        }
        catch
        {
            $errMsg = "Konfigurationsfehler: $($_.Exception.Message)"
            Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level 'ERROR'
            $result.ConfigResult  = 'Failed'
            $result.OverallStatus = 'PartialSuccess'   # Installiert, aber Konfig fehlgeschlagen
            $result.Message       = "Installation: $($result.InstallResult) | Konfiguration: Failed | $errMsg"
            if ($EnableException) { throw }
            Write-Error $errMsg
        }

        Write-Host ""
        Write-Host "[$ComputerName] $functionName : $($result.OverallStatus)" `
                   -ForegroundColor $(if ($result.OverallStatus -eq 'Success') { 'Green' } else { 'Yellow' })

        return $result
    }
}