Public/Install-sqmAdModule.ps1

<#
.SYNOPSIS
    Ensures that the ActiveDirectory PowerShell module (RSAT) is installed.
 
.DESCRIPTION
    First checks whether the ActiveDirectory module is already available.
    If not, the function attempts installation using four methods in the following
    order (fallback chain):
 
        1. Windows Capability (Add-WindowsCapability)
           Target: Windows 10/11 clients and Windows Server 2019+
           Package: Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0
 
        2. Windows Feature (Install-WindowsFeature)
           Target: Windows Server (all versions with ServerManager)
           Feature: RSAT-AD-PowerShell
 
        3. DISM (dism.exe /Online /Add-Capability)
           Target: older systems or environments without ServerManager/PS cmdlets
           Capability: Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0
 
        4. PSGallery (Install-Module ActiveDirectory)
           Target: systems with internet access and PSGallery access, when all
                   other methods are unavailable or failed.
           Scope: first CurrentUser, then AllUsers.
           Prerequisite: NuGet provider >= 2.8.5.201 (installed automatically if missing).
 
    Each method is only attempted if the responsible cmdlets or tool are present
    on the system. If a method fails, the next one is tried.
 
    After successful installation, Import-Module ActiveDirectory is run
    to load the module into the current session.
 
    Permission note:
        All installation methods require local administrator rights.
        The function checks this beforehand and returns an informative error.
 
.PARAMETER SkipIfPresent
    If $true (default) and the module is already present, $true is returned
    immediately without attempting installation.
    Set to $false to force a re-import.
 
.PARAMETER ContinueOnError
    When set, the function returns $false on failed installation instead of throwing an error.
 
.PARAMETER EnableException
    When set, the function throws an exception on failed installation
    (overrides ContinueOnError).
 
.PARAMETER WhatIf
    Shows which installation method would be attempted, without executing it.
 
.PARAMETER Confirm
    Request confirmation before installation.
 
.OUTPUTS
    [bool] - $true if the module is available and loaded at the end,
             $false if installation failed and ContinueOnError is set.
 
.EXAMPLE
    Install-sqmAdModule
 
    Checks whether the AD module is present and installs it if necessary.
 
.EXAMPLE
    Install-sqmAdModule -ContinueOnError
 
    Returns $false if installation fails instead of throwing an exception.
 
.EXAMPLE
    if (-not (Install-sqmAdModule -ContinueOnError))
    {
        Write-Warning "AD module not available - AD check will be skipped."
    }
 
.NOTES
    Prerequisites : Invoke-sqmLogging, local administrator rights
    Tested systems: Windows 10/11, Windows Server 2016/2019/2022
    DISM fallback : Requires internet access or a WSUS/SCCM source
                     (Windows Update must be reachable).
    PSGallery : Requires internet access and access to gallery.powershellgallery.com.
                     NuGet provider is installed automatically if missing.
                     In isolated environments this method will fail as expected.
    Restart : None of the methods requires a restart.
#>

function Install-sqmAdModule
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory = $false)]
        [bool]$SkipIfPresent = $true,
        [Parameter(Mandatory = $false)]
        [switch]$ContinueOnError,
        [Parameter(Mandatory = $false)]
        [switch]$EnableException
    )
    
    begin
    {
        $functionName = $MyInvocation.MyCommand.Name
        $adCapabilityName = 'Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0'
        $adFeatureName = 'RSAT-AD-PowerShell'
        
        # Hilfsfunktion: strukturierter Fehlerrueckgabe
        function _Fail
        {
            param ([string]$Message)
            Invoke-sqmLogging -Message $Message -FunctionName $functionName -Level 'ERROR'
            if ($EnableException) { throw $Message }
            Write-Warning $Message
            return $false
        }
    }
    
    process
    {
        # ?? 1. Bereits vorhanden? ?????????????????????????????????????????????
        $alreadyAvailable = [bool](Get-Module -ListAvailable -Name ActiveDirectory -ErrorAction SilentlyContinue)
        
        if ($alreadyAvailable -and $SkipIfPresent)
        {
            Invoke-sqmLogging -Message "ActiveDirectory-Modul bereits vorhanden - kein Installationsversuch." `
                              -FunctionName $functionName -Level 'INFO'
            Import-Module ActiveDirectory -ErrorAction SilentlyContinue
            return $true
        }
        
        # ?? 2. Administratorrechte pruefen ?????????????????????????????????????
        $currentPrincipal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
        $isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
        
        if (-not $isAdmin)
        {
            return (_Fail "Keine lokalen Administratorrechte - RSAT-Installation nicht moeglich. Starte PowerShell als Administrator.")
        }
        
        Invoke-sqmLogging -Message "ActiveDirectory-Modul nicht gefunden - starte Installationsversuch." `
                          -FunctionName $functionName -Level 'INFO'
        
        $installed = $false
        
        # ??????????????????????????????????????????????????????????????????????
        # Methode 1: Windows Capability (Add-WindowsCapability)
        # Windows 10/11 Clients und Windows Server 2019+
        # ??????????????????????????????????????????????????????????????????????
        $hasCapabilityCmd = [bool](Get-Command Add-WindowsCapability -ErrorAction SilentlyContinue)
        
        if (-not $installed -and $hasCapabilityCmd)
        {
            $action = "RSAT AD-Modul via Add-WindowsCapability installieren ($adCapabilityName)"
            if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, $action))
            {
                try
                {
                    Invoke-sqmLogging -Message "Methode 1 (Capability): $action" `
                                      -FunctionName $functionName -Level 'INFO'
                    
                    # Pruefen ob Capability bereits im Installed-State ist
                    $capState = Get-WindowsCapability -Online -Name $adCapabilityName -ErrorAction Stop
                    
                    if ($capState.State -eq 'Installed')
                    {
                        Invoke-sqmLogging -Message "Methode 1: Capability bereits installiert - nur Import." `
                                          -FunctionName $functionName -Level 'INFO'
                        $installed = $true
                    }
                    else
                    {
                        $capResult = Add-WindowsCapability -Online -Name $adCapabilityName -ErrorAction Stop
                        if ($capResult.RestartNeeded -eq $false -or $null -eq $capResult.RestartNeeded)
                        {
                            Invoke-sqmLogging -Message "Methode 1 (Capability): Erfolgreich installiert." `
                                              -FunctionName $functionName -Level 'INFO'
                            $installed = $true
                        }
                        else
                        {
                            Invoke-sqmLogging -Message "Methode 1 (Capability): Installiert, aber Neustart empfohlen." `
                                              -FunctionName $functionName -Level 'WARNING'
                            Write-Warning "RSAT-Installation abgeschlossen, aber ein Neustart wird empfohlen."
                            $installed = $true
                        }
                    }
                }
                catch
                {
                    Invoke-sqmLogging -Message "Methode 1 (Capability) fehlgeschlagen: $($_.Exception.Message) - versuche Methode 2." `
                                      -FunctionName $functionName -Level 'WARNING'
                }
            }
            else
            {
                Invoke-sqmLogging -Message "WhatIf: Methode 1 (Capability) wuerde ausgefuehrt." `
                                  -FunctionName $functionName -Level 'VERBOSE'
                return $true # WhatIf ? so tun als ob erfolgreich
            }
        }
        else
        {
            if (-not $hasCapabilityCmd)
            {
                Invoke-sqmLogging -Message "Methode 1 (Capability): Add-WindowsCapability nicht verfuegbar - uebersprungen." `
                                  -FunctionName $functionName -Level 'INFO'
            }
        }
        
        # ??????????????????????????????????????????????????????????????????????
        # Methode 2: Windows Feature (Install-WindowsFeature)
        # Windows Server (alle Versionen mit ServerManager)
        # ??????????????????????????????????????????????????????????????????????
        $hasFeatureCmd = [bool](Get-Command Install-WindowsFeature -ErrorAction SilentlyContinue)
        
        if (-not $installed -and $hasFeatureCmd)
        {
            $action = "RSAT AD-Modul via Install-WindowsFeature installieren ($adFeatureName)"
            if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, $action))
            {
                try
                {
                    Invoke-sqmLogging -Message "Methode 2 (WindowsFeature): $action" `
                                      -FunctionName $functionName -Level 'INFO'
                    
                    $featureResult = Install-WindowsFeature -Name $adFeatureName `
                                                            -IncludeManagementTools -ErrorAction Stop
                    
                    if ($featureResult.Success)
                    {
                        Invoke-sqmLogging -Message "Methode 2 (WindowsFeature): Erfolgreich installiert." `
                                          -FunctionName $functionName -Level 'INFO'
                        $installed = $true
                        
                        if ($featureResult.RestartNeeded -eq 'Yes')
                        {
                            Write-Warning "RSAT-Installation abgeschlossen, aber ein Neustart wird empfohlen."
                            Invoke-sqmLogging -Message "Methode 2: Neustart empfohlen." `
                                              -FunctionName $functionName -Level 'WARNING'
                        }
                    }
                    else
                    {
                        Invoke-sqmLogging -Message "Methode 2 (WindowsFeature): Install-WindowsFeature ohne Success-Flag - versuche Methode 3." `
                                          -FunctionName $functionName -Level 'WARNING'
                    }
                }
                catch
                {
                    Invoke-sqmLogging -Message "Methode 2 (WindowsFeature) fehlgeschlagen: $($_.Exception.Message) - versuche Methode 3." `
                                      -FunctionName $functionName -Level 'WARNING'
                }
            }
        }
        else
        {
            if (-not $installed -and -not $hasFeatureCmd)
            {
                Invoke-sqmLogging -Message "Methode 2 (WindowsFeature): Install-WindowsFeature nicht verfuegbar - uebersprungen." `
                                  -FunctionName $functionName -Level 'INFO'
            }
        }
        
        # ??????????????????????????????????????????????????????????????????????
        # Methode 3: DISM (dism.exe /Online /Add-Capability)
        # Fallback fuer aeltere Systeme ohne PS-Cmdlets
        # ??????????????????????????????????????????????????????????????????????
        $dismPath = "$env:SystemRoot\System32\dism.exe"
        $hasDism = Test-Path $dismPath
        
        if (-not $installed -and $hasDism)
        {
            $action = "RSAT AD-Modul via DISM installieren ($adCapabilityName)"
            if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, $action))
            {
                try
                {
                    Invoke-sqmLogging -Message "Methode 3 (DISM): $action" `
                                      -FunctionName $functionName -Level 'INFO'
                    
                    $dismArgs = @(
                        '/Online',
                        '/Add-Capability',
                        "/CapabilityName:$adCapabilityName",
                        '/Quiet',
                        '/NoRestart'
                    )
                    
                    $dismProcess = Start-Process -FilePath $dismPath `
                                                 -ArgumentList $dismArgs `
                                                 -Wait -PassThru -NoNewWindow `
                                                 -ErrorAction Stop
                    
                    if ($dismProcess.ExitCode -eq 0)
                    {
                        Invoke-sqmLogging -Message "Methode 3 (DISM): Erfolgreich installiert (ExitCode 0)." `
                                          -FunctionName $functionName -Level 'INFO'
                        $installed = $true
                    }
                    elseif ($dismProcess.ExitCode -eq 3010)
                    {
                        # 3010 = Erfolg, aber Neustart erforderlich
                        Write-Warning "RSAT via DISM installiert (ExitCode 3010) - Neustart empfohlen."
                        Invoke-sqmLogging -Message "Methode 3 (DISM): ExitCode 3010 - Neustart empfohlen." `
                                          -FunctionName $functionName -Level 'WARNING'
                        $installed = $true
                    }
                    else
                    {
                        Invoke-sqmLogging -Message "Methode 3 (DISM): ExitCode $($dismProcess.ExitCode) - Installation fehlgeschlagen." `
                                          -FunctionName $functionName -Level 'WARNING'
                    }
                }
                catch
                {
                    Invoke-sqmLogging -Message "Methode 3 (DISM) fehlgeschlagen: $($_.Exception.Message)" `
                                      -FunctionName $functionName -Level 'WARNING'
                }
            }
        }
        else
        {
            if (-not $installed -and -not $hasDism)
            {
                Invoke-sqmLogging -Message "Methode 3 (DISM): dism.exe nicht gefunden - uebersprungen." `
                                  -FunctionName $functionName -Level 'WARNING'
            }
        }
        
        # ??????????????????????????????????????????????????????????????????????
        # Methode 4: PSGallery (Install-Module ActiveDirectory)
        # Letzter Ausweg - setzt NuGet-Provider und PSGallery-Zugang
        # voraus. Nicht auf allen Umgebungen verfuegbar/erlaubt.
        # Schreibt in den CurrentUser-Scope um ohne Admin auszukommen,
        # versucht AllUsers wenn CurrentUser fehlschlaegt.
        # ??????????????????????????????????????????????????????????????????????
        $hasInstallModule = [bool](Get-Command Install-Module -ErrorAction SilentlyContinue)
        
        if (-not $installed -and $hasInstallModule)
        {
            $action = "ActiveDirectory-Modul via PSGallery installieren (Install-Module)"
            if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, $action))
            {
                try
                {
                    Invoke-sqmLogging -Message "Methode 4 (PSGallery): $action" `
                                      -FunctionName $functionName -Level 'INFO'
                    
                    # NuGet-Provider sicherstellen (PSGallery-Voraussetzung)
                    $nuget = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue
                    if (-not $nuget -or $nuget.Version -lt [Version]'2.8.5.201')
                    {
                        Invoke-sqmLogging -Message "Methode 4: NuGet-Provider wird installiert." `
                                          -FunctionName $functionName -Level 'INFO'
                        Install-PackageProvider -Name NuGet -MinimumVersion '2.8.5.201' `
                                                -Force -Scope CurrentUser -ErrorAction Stop | Out-Null
                    }
                    
                    # PSGallery als vertrauenswuerdig setzen (temporaer fuer diese Session)
                    $psGalleryTrusted = (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue).InstallationPolicy -eq 'Trusted'
                    if (-not $psGalleryTrusted)
                    {
                        Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop
                        Invoke-sqmLogging -Message "Methode 4: PSGallery als Trusted gesetzt." `
                                          -FunctionName $functionName -Level 'INFO'
                    }
                    
                    # Erst CurrentUser versuchen (kein Admin noetig), dann AllUsers
                    $installScopes = @('CurrentUser', 'AllUsers')
                    foreach ($scope in $installScopes)
                    {
                        try
                        {
                            Invoke-sqmLogging -Message "Methode 4 (PSGallery): Install-Module Scope=$scope" `
                                              -FunctionName $functionName -Level 'INFO'
                            Install-Module -Name ActiveDirectory -Scope $scope `
                                           -Force -AllowClobber -ErrorAction Stop
                            $installed = $true
                            Invoke-sqmLogging -Message "Methode 4 (PSGallery): Erfolgreich installiert (Scope=$scope)." `
                                              -FunctionName $functionName -Level 'INFO'
                            break
                        }
                        catch
                        {
                            Invoke-sqmLogging -Message "Methode 4 (PSGallery) Scope=$scope fehlgeschlagen: $($_.Exception.Message)" `
                                              -FunctionName $functionName -Level 'WARNING'
                        }
                    }
                    
                    if (-not $installed)
                    {
                        Invoke-sqmLogging -Message "Methode 4 (PSGallery): Alle Scopes fehlgeschlagen." `
                                          -FunctionName $functionName -Level 'WARNING'
                    }
                }
                catch
                {
                    Invoke-sqmLogging -Message "Methode 4 (PSGallery) fehlgeschlagen: $($_.Exception.Message)" `
                                      -FunctionName $functionName -Level 'WARNING'
                }
            }
        }
        else
        {
            if (-not $installed -and -not $hasInstallModule)
            {
                Invoke-sqmLogging -Message "Methode 4 (PSGallery): Install-Module nicht verfuegbar - uebersprungen." `
                                  -FunctionName $functionName -Level 'WARNING'
            }
        }
        
        # ?? 3. Abschlusspruefung ???????????????????????????????????????????????
        if (-not $installed)
        {
            return (_Fail ("Alle Installationsmethoden fehlgeschlagen (Capability, WindowsFeature, DISM, PSGallery). " +
                    "RSAT AD-Modul konnte nicht installiert werden. " +
                    "Installiere es manuell: " +
                    "'Add-WindowsCapability -Online -Name $adCapabilityName' " +
                    "oder ueber die Serverrollen-Verwaltung (RSAT-AD-PowerShell)."))
        }
        
        # Modul nach Installation in Session laden
        $verifyAvailable = [bool](Get-Module -ListAvailable -Name ActiveDirectory -ErrorAction SilentlyContinue)
        if (-not $verifyAvailable)
        {
            return (_Fail "Installation scheinbar erfolgreich, aber ActiveDirectory-Modul danach nicht auffindbar.")
        }
        
        try
        {
            Import-Module ActiveDirectory -ErrorAction Stop
            Invoke-sqmLogging -Message "ActiveDirectory-Modul erfolgreich geladen." `
                              -FunctionName $functionName -Level 'INFO'
            return $true
        }
        catch
        {
            return (_Fail "Installation erfolgreich, aber Import-Module ActiveDirectory fehlgeschlagen: $($_.Exception.Message)")
        }
    }
}