NTS.Tools.MSConfigMgr.psm1

function Install-WADK {
    <#
        .Description
        this function can be used to install Windows ADK and Windows ADK PE
 
        .Parameter Latest
        use if you want the latest version
 
        .Parameter Features
        a list of Windows ADK to be installed
 
        .Parameter IncludeWinPE
        use if you want windows adk pe installed
 
        .Parameter Outpath
        path where the install and log files are saved
 
        .Example
        # installs windows adk and windows adk pe for configmgr site server
        Install-WADK -Latest -IncludeWinPE -Features OptionId.DeploymentTools, OptionId.UserStateMigrationTool
 
        .NOTES
        requires internet connection
    #>


    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName = 'Latest')]
        [switch]
        $Latest,

        [Parameter(ParameterSetName = 'Latest', Mandatory = $true)]
        [ValidateSet(
            "OptionId.ApplicationCompatibilityToolkit", 
            "OptionId.DeploymentTools",
            "OptionId.ImagingAndConfigurationDesigner",
            "OptionId.ICDConfigurationDesigner",
            "OptionId.UserStateMigrationTool",
            "OptionId.VolumeActivationManagementTool",
            "OptionId.WindowsPerformanceToolkit",
            "OptionId.UEVTools",
            "OptionId.AppmanSequencer",
            "OptionId.AppmanAutoSequencer",
            "OptionId.MediaeXperienceAnalyzer",
            "OptionId.MediaeXperienceAnalyzer",
            "OptionId.WindowsAssessmentToolkit"
        )]
        [string[]]
        $Features,
        
        [Parameter(ParameterSetName = 'Latest')]
        [switch]
        $IncludeWinPE,

        [Parameter(ParameterSetName = 'Latest')]
        [string]
        $Outpath = "C:\Programdata\NTS\Windows_ADK"
    )

    if ($Latest -eq $true) {
        $WADK_Download_URL_Latest = "https://go.microsoft.com/fwlink/?linkid=2196127"
        $WADK_PE_Download_URL_Latest = "https://go.microsoft.com/fwlink/?linkid=2196224"    
    }
    else {
        throw "other version than latest are currently not supported"
    }

    try {
        if ((Test-Path -Path $Outpath) -eq $false) {
            New-Item -Path $Outpath -ItemType Directory -Force | Out-Null
        }
        $Outpath = (Get-Item -Path $Outpath).FullName
    
        if ($Features.count -gt 1) {
            $Features | ForEach-Object {
                [string]$Features_Selected = $Features_Selected + " " + $PSItem
            }
        }
    
        $WADK_LatestPath = "$($Outpath)\adksetup-latest.exe"
        $WADK_LogPath = "$($Outpath)\install-adksetup-latest.log"
        $WADK_PE_LatestPath = "$($Outpath)\adkwinpesetup-latest.exe"
        $WADK_PE_LogPath = "$($Outpath)\install-adkwinpesetup-latest.log"

        # download
        Write-Output "$($env:COMPUTERNAME): downloading adk setup files"
        Start-FileDownload -DownloadURL $WADK_Download_URL_Latest -FileOutPath $WADK_LatestPath

        # install
        Write-Output "$($env:COMPUTERNAME): installing adk with the features $($Features_Selected)"
        $Process = Start-Process -FilePath $WADK_LatestPath -ArgumentList "/quiet /norestart /features $($Features_Selected) /l $($WADK_LogPath)" -NoNewWindow -Wait -PassThru
        if ($Process.ExitCode -ne 0) {
            throw "check log at $($WADK_LogPath)"
        }
    
        if ($IncludeWinPE -eq $true) {
            # download
            Write-Output "$($env:COMPUTERNAME): downloading adk pe setup files"
            Start-FileDownload -DownloadURL $WADK_PE_Download_URL_Latest -FileOutPath $WADK_PE_LatestPath
    
            # install
            Write-Output "$($env:COMPUTERNAME): installing adk pe"
            $Process = Start-Process -FilePath $WADK_PE_LatestPath -ArgumentList "/quiet /norestart /features OptionId.WindowsPreinstallationEnvironment /l $($WADK_PE_LogPath)" -NoNewWindow -Wait -PassThru
            if ($Process.ExitCode -ne 0) {
                throw "check log at $($WADK_PE_LogPath)"
            }
        }

        Start-FolderCleanUp -FolderToRemove $Outpath
    }
    catch {
        throw "something went wrong $($PSItem.Exception.Message)"
    }
}

function Initialize-CM_MP_Prereq {
    <#
        .Description
        use this function to install configmgr management point prerequesits
 
        .Example
        # install configmgr management point prerequesits
        Initialize-CM_MP_Prereq
 
        .NOTES
         
    #>


    $Features = @(
        "NET-Framework-Core"
        "FileAndStorage-Services"
        "Storage-Services"
        "Web-Server"
        "Web-WebServer"
        "Web-Common-Http"
        "Web-Default-Doc"
        "Web-Dir-Browsing"
        "Web-Http-Errors"
        "Web-Static-Content"
        "Web-Http-Redirect"
        "Web-DAV-Publishing"
        "Web-Health"
        "Web-Http-Logging"
        "Web-Custom-Logging"
        "Web-Log-Libraries"
        "Web-ODBC-Logging"
        "Web-Request-Monitor"
        "Web-Http-Tracing"
        "Web-Performance"
        "Web-Stat-Compression"
        "Web-Dyn-Compression"
        "Web-Security"
        "Web-Filtering"
        "Web-Basic-Auth"
        "Web-CertProvider"
        "Web-Client-Auth"
        "Web-Digest-Auth"
        "Web-Cert-Auth"
        "Web-IP-Security"
        "Web-Url-Auth"
        "Web-Windows-Auth"
        "Web-App-Dev"
        "Web-Net-Ext"
        "Web-Net-Ext45"
        "Web-AppInit"
        "Web-ASP"
        "Web-Asp-Net"
        "Web-Asp-Net45"
        "Web-CGI"
        "Web-ISAPI-Ext"
        "Web-ISAPI-Filter"
        "Web-Includes"
        "Web-WebSockets"
        "Web-Ftp-Server"
        "Web-Ftp-Service"
        "Web-Ftp-Ext"
        "Web-Mgmt-Tools"
        "Web-Mgmt-Console"
        "Web-Mgmt-Compat"
        "Web-Metabase"
        "Web-Lgcy-Mgmt-Console"
        "Web-Lgcy-Scripting"
        "Web-WMI"
        "Web-Scripting-Tools"
        "Web-Mgmt-Service"
        "NET-Framework-Features"
        "NET-Framework-Core"
        "NET-Framework-45-Features"
        "NET-Framework-45-Core"
        "NET-Framework-45-ASPNET"
        "NET-WCF-Services45"
        "NET-WCF-HTTP-Activation45"
        "NET-WCF-MSMQ-Activation45"
        "NET-WCF-Pipe-Activation45"
        "NET-WCF-TCP-Activation45"
        "NET-WCF-TCP-PortSharing45"
        "BITS"
        "BITS-IIS-Ext"
        "BITS-Compact-Server"
        "MSMQ"
        "MSMQ-Services"
        "MSMQ-Server"
        "Windows-Defender"
        "RDC"
        "RSAT"
        "RSAT-Feature-Tools"
        "RSAT-Bits-Server"
        "System-DataArchiver"
        "PowerShellRoot"
        "PowerShell"
        "PowerShell-V2"
        "WAS"
        "WAS-Process-Model"
        "WAS-Config-APIs"
        "WoW64-Support"
        "XPS-Viewer"
    )

    try {
        Write-Output "$($env:COMPUTERNAME): installing required features for configmgr management point"
        Install-WindowsFeature -Name $Features | Out-Null
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Initialize-CM_DP_Prereq {
    <#
        .Description
        use this function to install configmgr distribution point prerequesits
 
        .Example
        # install configmgr distribution point prerequesits
        Initialize-CM_DP_Prereq
 
        .NOTES
         
    #>


    $Features = @(
        "FileAndStorage-Services"
        "File-Services"
        "FS-FileServer"
        "Storage-Services"
        "Web-Server"
        "Web-WebServer"
        "Web-Common-Http"
        "Web-Default-Doc"
        "Web-Dir-Browsing"
        "Web-Http-Errors"
        "Web-Static-Content"
        "Web-Http-Redirect"
        "Web-Health"
        "Web-Http-Logging"
        "Web-Performance"
        "Web-Stat-Compression"
        "Web-Security"
        "Web-Filtering"
        "Web-Windows-Auth"
        "Web-App-Dev"
        "Web-ISAPI-Ext"
        "Web-Mgmt-Tools"
        "Web-Mgmt-Console"
        "Web-Mgmt-Compat"
        "Web-Metabase"
        "Web-WMI"
        "Web-Scripting-Tools"
        "NET-Framework-45-Features"
        "NET-Framework-45-Core"
        "NET-WCF-Services45"
        "NET-WCF-TCP-PortSharing45"
        "Windows-Defender"
        "RDC"
        "System-DataArchiver"
        "PowerShellRoot"
        "PowerShell"
        "WoW64-Support"
        "XPS-Viewer"
    )

    try {
        Write-Output "$($env:COMPUTERNAME): creating NO_SMS_ON_DRIVE.SMS on boot volume"
        if ((Test-Path -Path "$($env:SystemDrive)\NO_SMS_ON_DRIVE.SMS") -eq $false) {
            New-Item -Path "$($env:SystemDrive)\NO_SMS_ON_DRIVE.SMS" -ItemType File | Out-Null
        }
        Write-Output "$($env:COMPUTERNAME): installing required features for configmgr distribution point"
        Install-WindowsFeature -Name $Features | Out-Null
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Initialize-CM_SiteServer_Prereq {
    <#
        .Description
        use this function to install configmgr site server prerequesits
 
        .Example
        # install configmgr site server prerequesits
        Initialize-CM_SiteServer_Prereq
 
        .NOTES
         
    #>


    $Features = @(
        "RDC"
        "UpdateServices-RSAT"
        "NET-Framework-Features"
    )

    try {
        Write-Output "$($env:COMPUTERNAME): creating NO_SMS_ON_DRIVE.SMS on boot volume"
        if ((Test-Path -Path "$($env:SystemDrive)\NO_SMS_ON_DRIVE.SMS") -eq $false) {
            New-Item -Path "$($env:SystemDrive)\NO_SMS_ON_DRIVE.SMS" -ItemType File | Out-Null
        }
        Write-Output "$($env:COMPUTERNAME): installing required features for configmgr site server"
        Install-WindowsFeature -Name $Features -IncludeAllSubFeature | Out-Null  
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Initialize-CM_SUP_Prereq {
    <#
        .Description
        use this function to install configmgr software update point prerequesits
 
        .Example
        # install configmgr software update point prerequesits
        Initialize-CM_SUP_Prereq
 
        .NOTES
         
    #>


    try {
        Write-Output "$($env:COMPUTERNAME): installing required features for configmgr software update point"
        Install-WindowsFeature -Name RDC, UpdateServices-RSAT -IncludeAllSubFeature | Out-Null
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Add-CM_ADContainer {
    <#
        .Description
        use this function to create the system management container in ad and add permissions to the local server
 
        .Example
        # checks if the container exits and adds permissions
        Add-CM_ADContainer
 
        .NOTES
         
    #>


    try {
        Import-Module -Name "ActiveDirectory"
        $AD_DistinguishedName = (Get-ADDomain).DistinguishedName
        $CM_ContainerName = "SYSTEM MANAGEMENT"
    
        Write-Output "$($env:COMPUTERNAME): adding container 'SYSTEM MANAGEMENT'"
        if ($null -eq (Get-ADObject -Filter 'ObjectClass -eq "container"' -SearchBase "CN=System,$($AD_DistinguishedName)" | Where-Object -Property Name -eq $CM_ContainerName)) {
            New-ADObject -Name $CM_ContainerName -Path "CN=System,$($AD_DistinguishedName)" -Type Container
        }
    
        Write-Output "$($env:COMPUTERNAME): adding permissions for the ad container"
        $path = "AD:\CN=$($CM_ContainerName),CN=System,$($AD_DistinguishedName)"
        $ADCompObject = Get-ADComputer -Identity $env:COMPUTERNAME
        
        $adRights = [DirectoryServices.ActiveDirectoryRights]::GenericAll
        $accessType = [Security.AccessControl.AccessControlType]::Allow
        $inheritance = [DirectoryServices.ActiveDirectorySecurityInheritance]::All
        $fullAccessACE = New-Object -TypeName DirectoryServices.ActiveDirectoryAccessRule -ArgumentList @($ADCompObject.SID, $adRights, $accessType, $inheritance)
        
        $acl = Get-Acl -Path $path
        $acl.AddAccessRule($fullAccessACE)
        Set-Acl -Path $path -AclObject $acl
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Install-WSUS {
    <#
        .Description
        this will install the required functions for wsus and do the post install tasks
 
        .Parameter UseWID
        wsus with windows internal database
 
        .Parameter UseSQL
        wsus with mssql database
 
        .Parameter WSUSFilePath
        where should the file be stored
 
        .Parameter SQLInstance
        sql instance for wsus
 
        .Example
        # configures the specified network card
        Set-Interface -InterfaceObject $SFP10G_NICs[0] -IPAddress $CLU1_IPAddress -NetPrefix $NetPrefix -DefaultGateway $CLU_DefaultGateway -DNSAddresses $CLU_DNSAddresses -NewName "Datacenter-1"
 
        .NOTES
        https://smsagent.blog/2014/02/07/installing-and-configuring-wsus-with-powershell/
    #>


    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName = 'WID')]
        [switch]
        $UseWID,

        [Parameter(ParameterSetName = 'SQL')]
        [switch]
        $UseSQL,

        [Parameter(ParameterSetName = 'WID', Mandatory = $true)]
        [Parameter(ParameterSetName = 'SQL', Mandatory = $true)]
        [string]
        $WSUSFilePath,

        [Parameter(ParameterSetName = 'SQL', Mandatory = $true)]
        [string]
        $SQLInstance # "MyServer\MyInstance"
    )

    if ((Test-Path -Path $WSUSFilePath) -eq $false) {
        New-Item -Path $WSUSFilePath -ItemType Directory -Force | Out-Null
    }

    try {
        if ($UseWID -eq $true) {
            Write-Output "$($env:COMPUTERNAME): installing required features for wsus"
            Install-WindowsFeature UpdateServices -IncludeManagementTools -WarningVariable SilentlyContinue | Out-Null
    
            Write-Output "$($env:COMPUTERNAME): doing postinstall with wid"
            Start-Process -FilePath "$($env:ProgramFiles)\Update Services\Tools\wsusutil.exe" -ArgumentList "postinstall CONTENT_DIR=$($WSUSFilePath)" -NoNewWindow -Wait
        }
        elseif ($UseSQL -eq $true) {
            Write-Output "$($env:COMPUTERNAME): installing required features for wsus"
            Install-WindowsFeature -Name UpdateServices-Services, UpdateServices-DB -IncludeManagementTools -WarningVariable SilentlyContinue | Out-Null
    
            Write-Output "$($env:COMPUTERNAME): doing postinstall with sql instance $($SQLInstance)"
            Start-Process -FilePath "$($env:ProgramFiles)\Update Services\Tools\wsusutil.exe" -ArgumentList "postinstall SQL_INSTANCE_NAME=$($SQLInstance) CONTENT_DIR=$($WSUSFilePath)"  -NoNewWindow -Wait
        }
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Confirm-CM_Prerequisites {
    <#
        .Description
        this function will search for the configmgr install volume and run the prerequisite checks for a site server
 
        .Parameter PrereqchkFilePath
        path to the prereqchk.exe
 
        .Parameter CM_SiteServerFQDN
        fqdn of the site server
 
        .Parameter CM_SQL_Site_Instance
        database server with instance name, eg. <fqdn of the site server>\<instancename>
 
        .Example
        # this will run the checks and throw an error if something is not passed
        Confirm-CM_Prerequisites -CM_SiteServerFQDN $CM_SiteServerFQDN -CM_SQL_Site_Instance ($CM_SiteServerFQDN + "\" + $using:CM_SQL_Site_InstanceName)
 
        .NOTES
        https://learn.microsoft.com/en-us/mem/configmgr/core/servers/deploy/install/prerequisite-checker
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        $PrereqchkFilePath,

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

        [Parameter(Mandatory = $true)]
        [string]
        $CM_SQL_Site_Instance
    )
    
    $CM_PrereqchkLogFilePath = "$($env:SystemDrive)\ConfigMgrPrereq.log"

    if ($PrereqchkFilePath -eq "") {
        $CM_SetupVolumes = Get-CM_Setup_Volume
        if ($CM_SetupVolumes.DriveLetter.count -eq 1) {
            $CM_Prereqchk_Filepath = "$(($CM_SetupVolumes).DriveLetter):\SMSSETUP\BIN\X64\prereqchk.exe"
        }
        else {
            throw "there are more than one or less than one installation media for configmgr"
        }
    }
    else {
        if (Test-Path -Path $PrereqchkFilePath) {
            $CM_Prereqchk_Filepath = $PrereqchkFilePath
        }
        else {
            throw "cannot find prereqchk.exe at $($PrereqchkFilePath)"
        }
    }

    if (Test-Path -Path $CM_PrereqchkLogFilePath) {
        Remove-Item -Path $CM_PrereqchkLogFilePath -Force | Out-Null
    }
    
    try {
        Write-Output "$($env:COMPUTERNAME): checking prerequisites for the site server role & admin console"
        Start-Process -FilePath $CM_Prereqchk_Filepath -ArgumentList "/NOUI /PRI /SDK $($CM_SiteServerFQDN) /SQL $CM_SQL_Site_Instance /SCP" -Wait -NoNewWindow
        Start-Process -FilePath $CM_Prereqchk_Filepath -ArgumentList "/NOUI /ADMINUI" -Wait -NoNewWindow
    }
    catch {
        throw "failed to run $($CM_Prereqchk_Filepath) - $($PSItem.Exception.Message)"
    }

    $Content = Get-Content -Path $CM_PrereqchkLogFilePath
    $SuccessMessage = $Content -like "*Prerequisite checking is completed.*"
    $FailureMessage = $Content -like "*ERROR:*"
    if ($null -eq $SuccessMessage[0] -and $null -ne $FailureMessage[0]) {
        if ($FailureMessage -like "*ERROR: Failed to connect to SQL Server 'master' db.*" -and $FailureMessage.Count -gt 2) {
            throw "found errors in log $($CM_PrereqchkLogFilePath):`n$($FailureMessage)"
        }
    }
    Write-Output "$($env:COMPUTERNAME): all prerequisites are met for configmgr installation"
}

function Uninstall-ConfigMgrAgent {
    <#
        .Description
        this function uninstalls the configmgr agent
 
        .Example
        # this function uninstalls the configmgr agent
        Uninstall-ConfigMgrAgent
 
        .NOTES
        https://learn.microsoft.com/en-us/mem/configmgr/core/servers/deploy/install/prerequisite-checker
    #>


    $SkipCleanup = $false

    try {
        $CCMExecServiceName = "CcmExec"
        $CCMSetupFilePath = "$($env:windir)\ccmsetup\ccmsetup.exe"
        if ($null -ne (Get-Service -Name $CCMExecServiceName -ErrorAction SilentlyContinue) -or (Test-Path -Path $CCMSetupFilePath)) {
            Write-Output "$($env:COMPUTERNAME): starting configmgr Agent uninstall"
            Start-Process -FilePath $CCMSetupFilePath -ArgumentList "/uninstall" -Wait -NoNewWindow

            $LogFileContent = Get-Content -Path "$($env:windir)\ccmsetup\logs\CCMSetup.log"
            $SuccesMessage = $LogFileContent -like "*[LOG[Uninstall succeeded.]LOG]*"
        }
        else {
            Write-Output "$($env:COMPUTERNAME): Service $($CCMExecServiceName) not found and no ccmsetup.exe, skipping"
            $SkipCleanup = $true
        }
    }
    catch {
        throw "$($env:COMPUTERNAME): error while uninstalling the agent - $($PSItem.Exception.Message)"
    }
    try {
        if ($SkipCleanup -eq $false) {
            if ($SuccesMessage.Count -gt 0) {
                Write-Output "$($env:COMPUTERNAME): finished configmgr Agent uninstall"
                Write-Output "$($env:COMPUTERNAME): doing cleanup"
                if (Test-Path -Path "$($env:windir)\CCM") {
                    $Items = Get-ChildItem -Path "$($env:windir)\CCM" 
                    $Items | ForEach-Object {
                        if ((Test-FileLock -Path $PSItem.FullName) -ne $true) {
                            Remove-Item -Path $PSItem.FullName -Force -Recurse | Out-Null
                        }
                    }
                }
                if (Test-Path -Path "$($env:windir)\ccmsetup") {
                    Remove-Item -Path "$($env:windir)\ccmsetup" -Force -Recurse | Out-Null
                }
                Write-Output "$($env:COMPUTERNAME): finished doing cleanup"
            }
            else {
                throw "uninstall was not successful $($PSItem.Exception.Message)"
            }
        }
    }
    catch {
        throw "$($env:COMPUTERNAME): error doing cleanup - $($PSItem.Exception.Message)"
    }
}

function Get-CM_Setupfiles {
    <#
        .Description
        downloads the eval setup of configmgr current branch
 
        .Parameter Version
        version of the iso
 
        .Parameter Outpath
        path where the setup file is stored
 
        .Example
        # stores the setup file to $Outpath
        Get-CM_Setupfiles -Version 2303 -Outpath $Outpath
 
        .NOTES
        downloads the configmgr current branch eval setup
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateSet("current", "2303")]
        [string]
        $Version = "current",

        [Parameter(Mandatory = $true)]
        [string]
        $Outpath
    )

    switch ($Version) {
        "current" { $DownloadURL = "https://go.microsoft.com/fwlink/p/?LinkID=2195628&clcid=0x409&culture=en-us&country=us" }
        "2303" { $DownloadURL = "https://download.microsoft.com/download/0/0/1/001d97e2-c427-4d4b-ad30-1556ee0ff1b0/MCM_Configmgr_2303.exe?culture=en-us&country=us" }
        Default { throw "no version was selected" }
    }
    
    if ((Test-Path -Path $Outpath) -eq $false) {
        New-Item -Path $Outpath -ItemType Directory -Force | Out-Null
    }
    $Outpath = (Get-Item -Path $Outpath).FullName
    $SetupPath = "ConfigMgr-$($Version)-CB-Eval.exe"
    $SetupFullPath = "$($Outpath)\$($SetupPath)"
    
    try {
        Start-FileDownload -DownloadURL $DownloadURL -FileOutPath $SetupFullPath
        Write-Output "$($env:COMPUTERNAME): finished download, starting extraction to $($Outpath)"
        $Arguments = "/auto `"$($Outpath)`""
        Start-Process -FilePath $SetupFullPath -ArgumentList $Arguments -NoNewWindow -Wait
        Write-Output "$($env:COMPUTERNAME): finished, setup files can be found at $($Outpath)"
    }
    catch {
        throw "error downloading eval setup: $($PSItem.Exception.Message)"
    }
}

function Get-CM_PrerequisiteFiles {
    <#
        .Description
        this function calls SMSSETUP\BIN\X64\Setupdl.exe from configmgr iso
 
        .Parameter SetupdlFilePath
        path to the Setupdl.exe
 
        .Parameter Outpath
        save path of the downloaded files
 
        .Example
        # downloads the files to $($env:SystemDrive)\Temp\ConfigMgr\SetupFiles
        Get-CM_PrerequisiteFiles -SetupdlFilePath "$($using:LocalConfigMgrSetupPath)\SMSSETUP\BIN\X64\setupdl.exe" -Outpath $PrerequisitePath
 
        .NOTES
        https://learn.microsoft.com/en-us/mem/configmgr/core/servers/deploy/install/setup-downloader
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        $SetupdlFilePath,

        [Parameter(Mandatory = $true)]
        [string]
        $Outpath
    )

    try {
        if ((Test-Path -Path $Outpath) -eq $false) {
            New-Item -Path $Outpath -ItemType Directory -Force | Out-Null
        }
        $Outpath = Resolve-Path $Outpath
        $LogFilePath = "$($env:SystemDrive)\ConfigMgrSetup.log"
        if ($SetupdlFilePath -eq "") {
            $CM_SetupVolumes = Get-CM_Setup_Volume
            if ($CM_SetupVolumes.DriveLetter.count -eq 1) {
                $CM_SetupFileDownloaderPath = "$(($CM_SetupVolumes).DriveLetter):\SMSSETUP\BIN\X64\Setupdl.exe"
            }
            else {
                throw "there are more than one or less than one installation media for configmgr"
            }
        }
        else {
            if (Test-Path -Path $SetupdlFilePath) {
                $CM_SetupFileDownloaderPath = $SetupdlFilePath
            }
            else {
                throw "cannot find Setupdl.exe at $($SetupdlFilePath)"
            }
        }
    
        if (Test-Path -Path $LogFilePath) {
            Remove-Item -Path $LogFilePath -Force | Out-Null
        }
            
        try {
            Write-Output "$($env:COMPUTERNAME): starting download of configmgr setup prerequisite files"
            Start-Process -FilePath $CM_SetupFileDownloaderPath -ArgumentList "/NoUI $($Outpath)" -Wait -NoNewWindow
        }
        catch {
            throw "failed to run $($LogFilePath) - $($PSItem.Exception.Message)"
        }
    
        $Content = Get-Content -Path $LogFilePath
        $SuccessMessage = $Content -like "*INFO: Setup downloader * FINISHED*"
        if ($null -eq $SuccessMessage[0]) {
            throw "no success message, check log $($LogFilePath)"
        }
        Write-Output "$($env:COMPUTERNAME): finished download of configmgr setup files"
    }
    catch {
        throw "error downloading configmgr setup files - $($PSItem.Exception.Message)"
    }
}

function Get-CM_Setup_Volume {
    <#
        .Description
        this function searches all volumes for the setup.exe from the configmgr iso
 
        .Example
        # this will return the volume where confimgr setup is
        Uninstall-ConfigMgrAgent
 
        .NOTES
         
    #>


    try {
        $Volumes = Get-Volume | Where-Object -FilterScript { $PSItem.DriveLetter -NE "C" -and $null -ne $PSItem.DriveLetter }
        $Volumes | ForEach-Object {
            if (Test-Path -Path "$($PSItem.DriveLetter):\SMSSETUP\BIN\X64\setup.exe") {
                return $PSItem
            }
        }        
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Initialize-CM_Schema_To_AD {
    <#
        .Description
        this function extends the schema using extadsch.exe for the configmgr
 
        .Parameter ExtadschFilePath
        path to the extadsch.exe
 
        .Example
        # ad schema will be prepared for configmgr
        Initialize-CM_Schema_To_AD
 
        .NOTES
        should be run on the siteserver with domain admin privileges
        temporarily the current user is added to the schema admin group
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        $ExtadschFilePath
    )

    try {
        if (((Get-ADGroupMember -Identity 'Schema Admins').Name -eq $env:USERNAME) -ne $true) {
            Write-Output "$($env:COMPUTERNAME): adding current user to schema admins"
            Add-ADGroupMember -Identity 'Schema Admins' -Members $env:USERNAME
        }

        if ($ExtadschFilePath -eq "") {
            $CM_SetupVolumes = Get-CM_Setup_Volume
            if ($CM_SetupVolumes.DriveLetter.count -eq 1) {
                $CM_Extadsch_Filepath = "$(($CM_SetupVolumes).DriveLetter):\SMSSETUP\BIN\X64\extadsch.exe"
            }
            else {
                throw "there are more than one or less than one installation media for configmgr"
            }
        }
        else {
            if (Test-Path -Path $ExtadschFilePath) {
                $CM_Extadsch_Filepath = $ExtadschFilePath
            }
            else {
                throw "cannot find extadsch.exe at $($ExtadschFilePath)"
            }
        }
        
        Write-Output "$($env:COMPUTERNAME): extending schema"
        Start-Process -FilePath $CM_Extadsch_Filepath -Wait -NoNewWindow

        $LogFilePath = "$($env:SystemDrive)\ExtADSch.log"
        $LogFileContent = Get-Content -Path $LogFilePath
        $SuccessMessage = $LogFileContent -like "*Successfully extended the Active Directory schema.*"
        $FailedMessages = $LogFileContent -like "*Failed to create*"

        if ($null -ne $SuccessMessage[0]) {
            Write-Output "$($env:COMPUTERNAME): finished extending schema"
        }
        else {
            throw "something went wrong, check the log at $($LogFilePath):`n$($FailedMessages[0])"
        }

        if (((Get-ADGroupMember -Identity 'Schema Admins').Name -eq $env:USERNAME) -ne $true) {
            Write-Output "$($env:COMPUTERNAME): removing current user from schema admins"
            Remove-ADGroupMember -Identity 'Schema Admins' -Members $env:USERNAME -Confirm:$false
        }
    }
    catch {
        throw "error extending schema - $($PSItem.Exception.Message)"
    }
}

function Install-CM_SiteServer {
    <#
        .Description
        this function extends the schema using extadsch.exe for the configmgr
 
        .Parameter SetupPath
        path to the Setup.exe
 
        .Parameter SiteName
        FriendlyName of the Site
 
        .Parameter SiteCode
        sitecode
 
        .Parameter PrerequisitePath
        path to prerequisite files
 
        .Parameter SQLServer
        fqdn of the sql server, can be the local server
 
        .Parameter SQLInstanceName
        name of the instance
 
        .Example
        # this will start the installation of a site server
        Install-CM_SiteServer -SiteName $CM_SiteName `
            -SiteCode $CM_SiteCode `
            -PrerequisitePath $PrerequisitePath `
            -SQLServer $CM_Site_SQLServer `
            -SQLInstanceName $CM_SQL_Site_InstanceName
 
        .NOTES
        https://learn.microsoft.com/en-us/mem/configmgr/core/servers/deploy/install/command-line-options-for-setup
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        $SetupPath,

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

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

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

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

        [Parameter(Mandatory = $true)]
        [string]
        $SQLInstanceName
    )

    try {
        $SiteServer = $($env:COMPUTERNAME + "." + $env:USERDNSDOMAIN)
        $SetupIniPath = "$($env:ProgramData)\NTS\ConfigMgr\SetupConfig.ini"
        $LogFilePath = "$($env:SystemDrive)\ConfigMgrSetup.log"

        $ConfigurationIni = "[Identification]
Action=InstallPrimarySite
CDLatest=0
 
[Options]
ProductID=Eval
SiteCode=$($SiteCode)
SiteName=$($SiteName)
SMSInstallDir=$($env:SystemDrive)\Program Files\Microsoft Configuration Manager
SDKServer=$($env:COMPUTERNAME + "." + $env:USERDNSDOMAIN)
PrerequisiteComp=1
PrerequisitePath=$($PrerequisitePath)
AdminConsole=1
JoinCEIP=0
MobileDeviceLanguage=0
 
RoleCommunicationProtocol=HTTPorHTTPS
ClientsUsePKICertificate=0
                 
[SQLConfigOptions]
SQLServerName=$($SQLServer + "\" + $SQLInstanceName)
DatabaseName=$("CM_" + $SiteCode)
                 
[CloudConnectorOptions]
CloudConnector=1
CloudConnectorServer=$($SiteServer)
UseProxy=0
                 
[SABranchOptions]
SAActive=0
CurrentBranch=1
"


        New-Item -Path $SetupIniPath -ItemType File -Force | Out-Null
        Set-Content -Path $SetupIniPath -Value $ConfigurationIni

        Write-Output "$($env:COMPUTERNAME): starting configmgr site server installtion"
        Write-Output "$($env:COMPUTERNAME): to see the progress please view this log $($env:SystemDrive)\ConfigMgrSetup.log on the site server"
        Write-Output "$($env:COMPUTERNAME): this can take a while"

        if ($SetupPath -eq "") {
            $CM_SetupVolumes = Get-CM_Setup_Volume
            if ($CM_SetupVolumes.DriveLetter.count -eq 1) {
                Start-Process -FilePath "$(($CM_SetupVolumes).DriveLetter):\SMSSETUP\BIN\X64\setup.exe" -ArgumentList "/SCRIPT $($SetupIniPath)" -Wait -NoNewWindow
            }
            else {
                throw "there are more than one or less than one installation media for configmgr"
            }
        }
        else {
            if (Test-Path -Path $SetupPath) {
                Start-Process -FilePath $SetupPath -ArgumentList "/SCRIPT $($SetupIniPath)" -Wait -NoNewWindow
            }
            else {
                throw "cannot find the setup file at $($SetupPath)"
            }
        }

        $Content = Get-Content -Path $LogFilePath
        $SuccessMessage = $Content -like "*~===================== Completed Configuration Manager Server Setup =====================*"
        if ($null -eq $SuccessMessage[0]) {
            throw "no success message, check log $($LogFilePath)"
        }
        Write-Output "$($env:COMPUTERNAME): finished the configmgr site server installtion"
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Convert-CMSiteUpdateState {
    <#
    .DESCRIPTION
    this function can be used to convert configmgr update status into a string
 
    .Parameter State
    current state of the configmgr site update
 
    .NOTES
    source: https://learn.microsoft.com/en-us/troubleshoot/mem/configmgr/setup-migrate-backup-recovery/understand-troubleshoot-updates-servicing#complete-list-of-state-codes
 
    .EXAMPLE
    Converts the state from int to string (for humans)
    Convert-CMSiteUpdateState -State (Get-CMSiteUpdate -Fast -Name $UpdateName).State
    #>

    
    # https://learn.microsoft.com/en-us/troubleshoot/mem/configmgr/setup-migrate-backup-recovery/understand-troubleshoot-updates-servicing#complete-list-of-state-codes

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        $State
    )

    switch ($State) {
        "" { $Message = "no state was provided" }
        "2" { $Message = "UNKNOWN" }
        "0x0" { $Message = "UNKNOWN" }
        "0x2" { $Message = "ENABLED" }
        "262145" { $Message = "DOWNLOAD_IN_PROGRESS" }
        "262146" { $Message = "DOWNLOAD_SUCCESS" }
        "327679" { $Message = "DOWNLOAD_FAILED" }
        "327681" { $Message = "APPLICABILITY_CHECKING" }
        "327682" { $Message = "APPLICABILITY_SUCCESS" }
        "393213" { $Message = "APPLICABILITY_HIDE" }
        "393214" { $Message = "APPLICABILITY_NA" }
        "393215" { $Message = "APPLICABILITY_FAILED" }
        "65537" { $Message = "CONTENT_REPLICATING" }
        "65538" { $Message = "CONTENT_REPLICATION_SUCCESS" }
        "131071" { $Message = "CONTENT_REPLICATION_FAILED" }
        "131073" { $Message = "PREREQ_IN_PROGRESS" }
        "131074" { $Message = "PREREQ_SUCCESS" }
        "131075" { $Message = "PREREQ_WARNING" }
        "196607" { $Message = "PREREQ_ERROR" }
        "196609" { $Message = "INSTALL_IN_PROGRESS" }
        "196610" { $Message = "INSTALL_WAITING_SERVICE_WINDOW" }
        "196611" { $Message = "INSTALL_WAITING_PARENT" }
        "196612" { $Message = "INSTALL_SUCCESS" }
        "196613" { $Message = "INSTALL_PENDING_REBOOT" }
        "262143" { $Message = "INSTALL_FAILED" }
        "196614" { $Message = "INSTALL_CMU_VALIDATING" }
        "196615" { $Message = "INSTALL_CMU_STOPPED" }
        "196616" { $Message = "INSTALL_CMU_INSTALLFILES" }
        "196617" { $Message = "INSTALL_CMU_STARTED" }
        "196618" { $Message = "INSTALL_CMU_SUCCESS" }
        "196619" { $Message = "INSTALL_WAITING_CMU" }
        "262142" { $Message = "INSTALL_CMU_FAILED" }
        "196620" { $Message = "INSTALL_INSTALLFILES" }
        "196621" { $Message = "INSTALL_UPGRADESITECTRLIMAGE" }
        "196622" { $Message = "INSTALL_CONFIGURESERVICEBROKER" }
        "196623" { $Message = "INSTALL_INSTALLSYSTEM" }
        "196624" { $Message = "INSTALL_CONSOLE" }
        "196625" { $Message = "INSTALL_INSTALLBASESERVICES" }
        "196626" { $Message = "INSTALL_UPDATE_SITES" }
        "196627" { $Message = "INSTALL_SSB_ACTIVATION_ON" }
        "196628" { $Message = "INSTALL_UPGRADEDATABASE" }
        "196629" { $Message = "INSTALL_UPDATEADMINCONSOLE" }
        Default { $Message = "could not map the state '$($State)' to state message" }
    }
    return $Message
}

function Get-ConfigMgrSiteUpdate {
    <#
    .DESCRIPTION
    fetches the information for a configmgr site update
 
    .Parameter UpdateName
    name of the update
 
    .NOTES
 
    .EXAMPLE
    returns the info for $Updatename
    Get-ConfigMgrSiteUpdate -Updatename $UpdateName
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $UpdateName
    )
    
    try {
        do {
            try {
                $CMSiteUpdate = Get-CMSiteUpdate -Name $UpdateName -Fast
            }
            catch {
                if ($PSItem.Exception.Message -like "*The SMS Provider reported an error*") {
                    Write-Output "$($env:COMPUTERNAME): waiting on sms provider"
                }
                else {
                    throw $PSItem.Exception.Message
                }
            }
            Start-Sleep -Seconds 10
        } 
        while ( $Null -eq $CMSiteUpdate)

        return $CMSiteUpdate
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Write-CMSiteUpdateStatus {
    <#
    .DESCRIPTION
    shows current status of the update
 
    .Parameter UpdateObj
    A single object returned from get-cmsiteupdate
 
    .Parameter Detailed
    turns on more detailed output of the current status
 
    .NOTES
 
    .EXAMPLE
    # shows the current status of the configmgr site update unitil it reaches download_success
    Write-CMSiteUpdateStatus -UpdateObj $UpdateObj -Detailed
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.Object]
        $UpdateObj,

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

    try {
        try {
            $StatusMessage = Convert-CMSiteUpdateState -State $UpdateObj.State
            if ($Detailed) {
                $DetailStatusMessages = $UpdateObj | Get-CMSiteUpdateInstallStatus -Complete -Step All | Select-Object -Last 5 -Property Progress, orderid, SubStageName, Description | Format-Table -AutoSize
            }
        }
        catch {
            if ($PSItem.Exception.Message -like "*The SMS Provider reported an error.*") {
                Write-Output "$($env:COMPUTERNAME): waiting on sms provider"
            }
            else {
                throw $PSItem.Exception.Message
            }
        }
        Write-Output "$($env:COMPUTERNAME): $($UpdateName) - current status $($StatusMessage)"
        if ($Detailed -and $null -ne $DetailStatusMessages -and $DetailStatusMessages -ne "") {
            $DetailStatusMessages
        }
    }
    catch {
        throw "error checking status - $($PSItem.Message.Exception)"
    }
}

function Confirm-CMSiteUpdatePackageDownloaded {
    <#
    .DESCRIPTION
    writes the current status of configmgr site update until the update is downloaded
 
    .Parameter UpdateName
    name of the configmgr site update
 
    .Parameter Detailed
    turns on more detailed output of the current status
 
    .NOTES
 
    .EXAMPLE
    # shows the current status of the configmgr site update
    Confirm-CMSiteUpdatePackageDownloaded -UpdateName $UpdateToInstall.Name
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $UpdateName,

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

    # Define update check variables
    $CheckCount = 0
    $ServiceName = "SMS_EXECUTIVE"

    Write-Output "`n$($env:COMPUTERNAME): verifying update $($UpdateName) is downloaded"

    $StoppingStatus = "DOWNLOAD_SUCCESS", "UNKNOWN"

    do {
        if($Detailed) {
            Write-Output "---"
        }
        
        $UpdateCheckStart = Get-Date
        $CheckCount++

        $UpdateObj = Get-ConfigMgrSiteUpdate -UpdateName $UpdateName
        $CMUpdateStatus = Convert-CMSiteUpdateState -State $UpdateObj.State
        if ($CMUpdateStatus -ne "DOWNLOAD_SUCCESS") {
            if ($Detailed) {
                Write-Output "$($env:COMPUTERNAME): times checked: $($CheckCount)"
                Write-CMSiteUpdateStatus -UpdateObj $UpdateObj -Detailed
            }
            else {
                Write-CMSiteUpdateStatus -UpdateObj $UpdateObj
            }
            if ($CheckCount -eq 40) {
                Write-Output "$($env:COMPUTERNAME): downloading state detected for longer than $(((Get-Date) - $UpdateCheckStart).Minutes) minutes, restarting $($ServiceName) service"
                Restart-Service -Name $ServiceName -Force -Verbose:$false
            }
        }
        else {
            Write-Output "$($env:COMPUTERNAME): update package $($UpdateName) is available - current status: $($CMUpdateStatus)"
        }
        if ($CheckCount -ge 150) {
            throw "update is not available, please check manually"
        }
        Start-Sleep -Seconds 15
    }
    while ($StoppingStatus -notcontains $CMUpdateStatus)
}

function Confirm-CMSiteUpdatePrereqCheckFinished {
    <#
    .DESCRIPTION
    writes the current status of configmgr site update until the prereq checks are finished
 
    .Parameter UpdateName
    name of the configmgr site update
 
    .Parameter Detailed
    turns on more detailed output of the current status
 
    .NOTES
 
    .EXAMPLE
    # shows the current status of the configmgr site update
    Confirm-CMSiteUpdatePrereqCheckFinished -UpdateName $UpdateToInstall.Name
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $UpdateName,

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

    # Define update check variables
    $CheckCount = 0
    $ServiceName = "SMS_EXECUTIVE"
    $StoppingStatus = "PREREQ_ERROR", "PREREQ_WARNING", "INSTALL_IN_PROGRESS"

    Write-Output "`n$($env:COMPUTERNAME): verifying prerequisite checks for update $($UpdateName) are finished"

    do {
        if($Detailed) {
            Write-Output "---"
        }
        $UpdateCheckStart = Get-Date
        $CheckCount++

        $UpdateObj = Get-ConfigMgrSiteUpdate -UpdateName $UpdateName
        $CMUpdateStatus = Convert-CMSiteUpdateState -State $UpdateObj.State
        if ($StoppingStatus -notcontains $CMUpdateStatus) {
            if ($Detailed) {
                Write-Output "$($env:COMPUTERNAME): times checked: $($CheckCount)"
                Write-CMSiteUpdateStatus -UpdateObj $UpdateObj -Detailed
            }
            else {
                Write-CMSiteUpdateStatus -UpdateObj $UpdateObj
            }
            if ($CheckCount -eq 40) {
                Write-Output "$($env:COMPUTERNAME): no state change detected for longer than $(((Get-Date) - $UpdateCheckStart).Minutes) minutes, restarting $($ServiceName) service"
                Restart-Service -Name $ServiceName -Force -Verbose:$false
            }
        }
        else {
            Write-Output "$($env:COMPUTERNAME): prereq checks for $($UpdateName) finished - current status: $($CMUpdateStatus)"
        }

        if ($CheckCount -ge 150) {
            throw "prereq checks took to long, check manually"
        }
        Start-Sleep -Seconds 15
    }
    while ($StoppingStatus -notcontains $CMUpdateStatus)
    
    if ($CMUpdateStatus -eq "INSTALL_IN_PROGRESS") {
        Write-Output "$($env:COMPUTERNAME): installation was successfully initated for $($UpdateName), for more details, review the CMUpdate.log - current status: $(Convert-CMSiteUpdateState -State $CMUpdatePackage.State)"
    }
    elseif ($CMUpdateStatus -eq "PREREQ_ERROR") {
        Write-Output "$($env:COMPUTERNAME): prerequisite checks found some errors, please check manually - current status: $(Convert-CMSiteUpdateState -State $CMUpdatePackage.State)"
    }
    elseif ($CMUpdateStatus -eq "PREREQ_WARNING") {
        Write-Output "$($env:COMPUTERNAME): prerequisite checks found some warnings, please check manually - current status: $(Convert-CMSiteUpdateState -State $CMUpdatePackage.State)"
    }
}

function Confirm-CMSiteUpdatePackageInstallation {
    <#
    .DESCRIPTION
    checks the install status of a running configmgr update
 
    .Parameter UpdateName
    name of the configmgr site update
 
    .Parameter Detailed
    turns on more detailed output of the current status
 
    .NOTES
 
    .EXAMPLE
    # shows the current status of the configmgr site update
    Confirm-CMSiteUpdatePackageInstallation -UpdateName $UpdateToInstall.Name
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $UpdateName,
        
        [Parameter(Mandatory = $false)]
        [switch]
        $Detailed
    )

    $StoppingStatus = "INSTALL_SUCCESS", "INSTALL_FAILED"

    Write-Output "`n$($env:COMPUTERNAME): verifying installation of update $($UpdateName) is finished"
    do {
        if($Detailed) {
            Write-Output "---"
        }
        $UpdateObj = Get-ConfigMgrSiteUpdate -UpdateName $UpdateName
        $StatusMessage = Convert-CMSiteUpdateState -State $UpdateObj.State
        if ($Detailed) {
            Write-CMSiteUpdateStatus -UpdateObj $UpdateObj -Detailed
        }
        else {
            Write-CMSiteUpdateStatus -UpdateObj $UpdateObj
        }
        Start-Sleep -Seconds 15
    }
    while ($StoppingStatus -notcontains $StatusMessage)

    $UpdateObj = Get-ConfigMgrSiteUpdate -UpdateName $UpdateName
    $StatusMessage = Convert-CMSiteUpdateState -State $UpdateObj.State
    if ($StatusMessage -eq "INSTALL_SUCCESS") {
        Write-Output "$($env:COMPUTERNAME): installation of update $($UpdateName) is finished, post install steps are not finished yet."
        Write-Output "$($env:COMPUTERNAME): admin console updates may be required"

        # do {
        # if($Detailed) {
        # Write-Output "---"
        # }
        # $UpdateObj = Get-ConfigMgrSiteUpdate -UpdateName $UpdateName
        # $StatusMessage = Convert-CMSiteUpdateState -State $UpdateObj.State
        # Write-CMSiteUpdateStatus -UpdateObj $UpdateObj -Detailed
        # Start-Sleep -Seconds 15
        # }
        # while ($true)
        # Write-Output "$($env:COMPUTERNAME): post install of configmgr site update $($UpdateName) is finished"
    }
    else {
        Write-Output "$($env:COMPUTERNAME): current status $($StatusMessage)"
        throw "something went wrong - please check the logs"
    }
}