NTS-ConfigMgrTools.psm1

function New-VMVolume {
    <#
        .Description
        Creates a storage pool, virtual disk and volume intended for VMs
        RAID Level will always be 0, so be carefull
 
        .Parameter VMDriveLetter
        Letter for the volume
 
        .Parameter StoragePoolName
        Name of the StoragePool
 
        .Parameter VirtualDiskName
        Name of the VirtualDisk
 
        .Parameter VMVolumeName
        Name of the Volume
 
        .Example
        # Creates a volume with the letter W
        New-VMVolume -VMDriveLetter 'W'
 
        .NOTES
        - There must be at least one other disk in addition to disk 0.
        - If an NVMe disk is present, only this is taken
    #>


    param (
        [Parameter(Mandatory = $false)]
        [char]
        $VMDriveLetter = 'V',

        [Parameter(Mandatory = $false)]
        [string]
        $StoragePoolName = "Pool01",

        [Parameter(Mandatory = $false)]
        [string]
        $VirtualDiskName = "VDisk01",

        [Parameter(Mandatory = $false)]
        [string]
        $VMVolumeName = "VMs"
    )

    try {
        if ($null -eq (Get-StoragePool -FriendlyName $StoragePoolName -ErrorAction SilentlyContinue)) {
            $PhysicalDisks = Get-PhysicalDisk -CanPool $true | Where-Object -FilterScript { $PSitem.Bustype -ne "USB" }
            $NVMe_Devices = $PhysicalDisks | Where-Object -FilterScript { $PSItem.Bustype -eq "NVMe" -and $PSitem.Size -gt 256GB }
            $Non_NVMe_Devices = $PhysicalDisks | Where-Object -FilterScript { $PSItem.Bustype -ne "NVMe" }
            if ($null -ne $NVMe_Devices) {
                $SelectedDisks = $NVMe_Devices
            }
            else {
                $SelectedDisks = $Non_NVMe_Devices
            }
            $StorageSubSystemFriendlyName = (Get-StorageSubSystem -FriendlyName "*Windows*").FriendlyName
            Write-Output "create storage pool '$($StoragePoolName)'"
            New-StoragePool -StorageSubSystemFriendlyName $StorageSubSystemFriendlyName -FriendlyName $StoragePoolName -PhysicalDisks $SelectedDisks | Out-Null
            if ($null -eq (Get-VirtualDisk -FriendlyName $VirtualDiskName -ErrorAction SilentlyContinue)) {
                Write-Output "create vdisk '$($VirtualDiskName)'"
                New-VirtualDisk -StoragePoolFriendlyName $StoragePoolName -FriendlyName $VirtualDiskName -UseMaximumSize -ProvisioningType Fixed -ResiliencySettingName Simple | Out-Null
                Initialize-Disk -FriendlyName $VirtualDiskName -PartitionStyle GPT | Out-Null
                $VDiskNumber = (Get-Disk -FriendlyName $VirtualDiskName).Number
                Write-Output "create volume '$($VMVolumeName)'"
                New-Volume -DiskNumber $VDiskNumber -FriendlyName $VMVolumeName -FileSystem ReFS -DriveLetter $VMDriveLetter | Out-Null
            }
            else {
                Write-Output "Virtual disk '$($VirtualDiskName)' already exists - skipping"
            }
        }
        else {
            Write-Output "Pool '$($StoragePoolName)' already exists - skipping"
        }
    }
    catch {
        throw "error during creation of vm volume: $($PSItem.Exception.Message)"
    }
}

function New-VMVSwitch {
    <#
        .Description
        Creates a VM switch based on a network card with the Up state. 10Gbit NICs are preferred
 
        .Parameter Course_Shortcut
        Name of the VM Switch
 
        .Example
        # Creates a new VM Switch with the name IC
        New-VMVSwitch -Course_Shortcut 'IC'
 
        .NOTES
        there must be at least one nic with status 'up'
    #>


    param (
        # Course Shortcut
        [Parameter(Mandatory = $false)]
        [string]
        $Course_Shortcut = "LAN"
    )
    
    try {
        if ($null -eq (Get-VMSwitch -Name $Course_Shortcut -ErrorAction SilentlyContinue)) {
            $pNICs = Get-NetAdapter -Physical | Where-Object -Property Status -eq "UP"
            $10G_NICs = $pNICs | Where-Object -Property LinkSpeed -EQ "10 Gbps"
            $1G_NICs = $pNICs | Where-Object -Property LinkSpeed -EQ "1 Gbps"
    
            if ($10G_NICs) {
                $Selected_NIC = $10G_NICs[0]
            }
            elseif ($1G_NICs) {
                $Selected_NIC = $1G_NICs[0]
            }
            else {
                $Selected_NIC = (Get-NetAdapter -Physical | Where-Object -Property Status -eq "UP")[0]
            }
    
            Write-Output "create vswitch '$($Course_Shortcut)'"
            New-VMSwitch -Name $Course_Shortcut -NetAdapterName $Selected_NIC.Name -AllowManagementOS $false | Out-Null
            Add-VMNetworkAdapter -ManagementOS -SwitchName $Course_Shortcut -Name "vNIC-$($Course_Shortcut)"
            Rename-NetAdapter -Name $Selected_NIC.Name -NewName "pNIC-$($Course_Shortcut)"
        }
        else {
            Write-Output "virtual vswitch '$($Course_Shortcut)' already exists - skipping"
        }
    }
    catch {
        throw "error during creation of virtual switch: $($PSItem.Exception.Message)"
    }
}

function Register-VM_in_CM {
    <#
        .Description
        Registers the VM Objects in ConfigMgr with its MAC address for Required Deployments
 
        .Parameter VM_Config_Obj
        Object that contains multiple descriptive objects for deployment from a VM.
 
        .Parameter CM_Siteserver_FQDN
        FQDN of ConfigMgr
 
        .Parameter CM_Credentials
        Credentials of a user that can create/edit/delete CMDevices and add them to a Collection. Should be able to start a collection update
 
        .Example
        # Registers the VMs from $VM_Config with $CM_Siteserver_FQDN
        Register-VM_in_CM -VM_Config_Obj $VM_Config -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials
 
        .NOTES
        the VM_Config_Obj object, should be like `
        `$VM_Config_Obj = @{}
        `$VM_01 = @{
            Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1'
            RAM = `$RAM
            CPU = `$CPUCount
            CM_Collection_Name = `$CM_Collection_W11_Autopilot
            Credentials = `$VM_Credentials
            DiskSize = `$DynamicDiskSize
            MAC = ""
        }
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [hashtable]
        $VM_Config_Obj,

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

        [Parameter(Mandatory = $true)]
        [PSCredential]
        $CM_Credentials
    )

    Write-Output "`nstart registration of vms in configmgr"
    Write-Output "------"
    Confirm-VMPresence -VM_Config_Obj $VM_Config_Obj
    try {
        Invoke-Command -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials -ScriptBlock {
            $PSDriveName = "CM-PB2"
            $VM_Config_Obj = $using:VM_Config_Obj
            $CM_Collection_All_Systems_Name = "All Systems"

            # load configmgr ps module
            Import-Module -Name ConfigurationManager
            New-PSDrive -Name $PSDriveName -PSProvider "CMSite" -Root $using:CM_Siteserver_FQDN -Description "Primary site" | Out-Null
            Set-Location -Path "$($PSDriveName):\"

            # check destination collection existance
            foreach ($VM in $VM_Config_Obj.Keys | Sort-Object) {
                if ($null -eq (Get-CMCollection -Name $VM_Config_Obj.$VM.CM_Collection_Name)) {
                    throw "collection '$($VM_Config_Obj.$VM.CM_Collection_Name)' does not existing"
                }
            }
            
            # cleanup before creation
            foreach ($VM in $VM_Config_Obj.Keys | Sort-Object) {
                $Temp_CMDevice = Get-CMDevice -Name $VM
                if ($null -ne $Temp_CMDevice) {
                    
                    $Collections = Get-CimInstance -Namespace root/Sms/site_PB1 -ClassName SMS_FullCollectionMembership -Filter "ResourceID = $($Temp_CMDevice.ResourceID)"
                    if ($null -ne $Collections) {
                        Write-Output "$($VM): removing existing computer collection membership rules configmgr - macaddress '$($VM_Config_Obj.$VM.MAC)'"
                        $Collections | ForEach-Object {
                            $MembershipRule = Get-CMCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID | Where-Object -Property RuleName -EQ $CMDevice.Name
                            Remove-CMDeviceCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID -ResourceId $MembershipRule.ResourceID    
                        }
                    }
                    Write-Output "$($VM): removing existing computer info in configmgr - macaddress '$($VM_Config_Obj.$VM.MAC)'"
                    $Temp_CMDevice | Remove-CMDevice -Force -Confirm:$false
                }
            }
            Start-Sleep -Seconds 5

            # import device
            foreach ($VM in $VM_Config_Obj.Keys | Sort-Object) {
                Write-Output "$($VM): creating vm computer info in configmgr - macaddress '$($VM_Config_Obj.$VM.MAC)'"
                Import-CMComputerInformation -CollectionName $CM_Collection_All_Systems_Name -ComputerName $VM -MacAddress $VM_Config_Obj.$VM.MAC
            }

            # check vm collection membership
            foreach ($VM in $VM_Config_Obj.Keys | Sort-Object) {
                #region all systems collection
                $Device_Exists_In_AllDevices = $false
                $All_Devices_Counter = 0
                Write-Output "$($VM): checking collection memberships"
                while ($Device_Exists_In_AllDevices -eq $false -and $All_Devices_Counter -lt 30) {
                    $CMDevice = Get-CMDevice -Name $VM
                    if ($CMDevice.Name -eq $VM) {
                        $Device_Exists_In_AllDevices = $true
                    }
                    else {
                        Start-Sleep -Seconds 5
                        $All_Devices_Counter++
                    }
                    Write-Output "$($VM): device not found in collection '$($CM_Collection_All_Systems_Name)'"
                    if ($All_Devices_Counter -eq 12) {
                        Write-Output "$($VM): triggering membership update in Collection '$($CM_Collection_All_Systems_Name)'"
                        Start-Sleep -Seconds (10 + (Get-Random -Maximum 50 -Minimum 10))
                        Get-CMCollection -Name $CM_Collection_All_Systems_Name | Invoke-CMCollectionUpdate
                    }
                }
                if ($All_Devices_Counter -ge 30) {
                    throw "$($VM): could not find in the collection '$($CM_Collection_All_Systems_Name)'"
                }
                else {
                    Write-Output "$($VM): '$($CM_Collection_All_Systems_Name)' - found, continuing"
                }
                #endregion
            
                # create vm membership rule
                Add-CMDeviceCollectionDirectMembershipRule -CollectionName $VM_Config_Obj.$VM.CM_Collection_Name -ResourceID $CMDevice.ResourceID

                #region destination collection
                $Device_Exists_In_Specified_Collection = $false
                $Specified_Collection_Counter = 0
                while ($Device_Exists_In_Specified_Collection -eq $false -and $Specified_Collection_Counter -lt 30) {
                    $Collection_Direct_Members = Get-CMDeviceCollectionDirectMembershipRule -CollectionName $VM_Config_Obj.$VM.CM_Collection_Name | Where-Object RuleName -eq $VM
                    if ($null -ne $Collection_Direct_Members) {
                        $Device_Exists_In_Specified_Collection = $true
                    }
                    else {
                        Start-Sleep -Seconds 5
                        $Specified_Collection_Counter++
                    }
                    Write-Output "$($VM): device not found in collection '$($VM_Config_Obj.$VM.CM_Collection_Name)'"
                    if ($Specified_Collection_Counter -eq 20) {
                        Write-Output "$($VM): triggering membership update in Collection '$($VM_Config_Obj.$VM.CM_Collection_Name)'"
                        Start-Sleep -Seconds (10 + (Get-Random -Maximum 50 -Minimum 10))
                        Get-CMCollection -Name $VM_Config_Obj.$VM.CM_Collection_Name | Invoke-CMCollectionUpdate
                    }
                }
                if ($Specified_Collection_Counter -ge 30) {
                    throw "$($VM): could not find in the collection '$($VM_Config_Obj.$VM.CM_Collection_Name)'"
                }
                else {
                    Write-Output "$($VM): '$($VM_Config_Obj.$VM.CM_Collection_Name)' - found, continuing"
                }
                #endregion
            }
            Set-Location -Path $env:SystemDrive
            Remove-PSDrive -Name $PSDriveName
        }
        $SecondsToWait = 300
        Write-Output "`nfinished registration of vms, now waiting $($SecondsToWait) seconds for the configmgr database updates and stabilization"
        Start-Sleep -Seconds $SecondsToWait
    }
    catch {
        throw "error during registration of device infos in configmgr: $($PSItem.Exception.Message)"
    }
}

function Start-VM_Deployment {
    <#
        .Description
        Starts VMs
 
        .Parameter VM_Config_Obj
        Object that contains multiple descriptive objects for deployment from a VM.
 
        .Example
        # Starts VMs, based on objects in $VM_Config
        Start-VM_Deployment -VM_Config_Obj $VM_Config
 
        .NOTES
        the VM_Config_Obj object, should be like `
        `$VM_Config_Obj = @{}
        `$VM_01 = @{
            Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1'
            RAM = `$RAM
            CPU = `$CPUCount
            CM_Collection_Name = `$CM_Collection_W11_Autopilot
            Credentials = `$VM_Credentials
            DiskSize = `$DynamicDiskSize
            MAC = ""
        }
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [hashtable]
        $VM_Config_Obj
    )

    if ($null -eq $VM_Config_Obj) {
        throw "no VM_Config_Obj was supplied, please specify"
    }

    try {
        Write-Output "`nstarting vm deployments"
        Write-Output "------"
    
        foreach ($VM in $VM_Config_Obj.Keys | Sort-Object) {
            Write-Output "$($VM): starting deployment"
            Start-Sleep -Seconds 2
            Start-VM -VMName $VM
        }
    }
    catch {
        throw "error while starting vms: $($PSItem.Exception.Message)"
    }
}

function Confirm-VM_Deployment {
    <#
        .Description
        Checks the ConfigMgr database to see if the deployment of VM objects is complete.
 
        .Parameter VM_Config_Obj
        Object that contains multiple descriptive objects for deployment from a VM.
 
        .Parameter CM_Siteserver_FQDN
        FQDN of ConfigMgr
 
        .Parameter CM_Credentials
        Credentials of a user that can create/edit/delete CMDevices and add them to a Collection. Should be able to start a collection update
 
        .Example
        # Checks the ConfigMgr database to see if the deployment of VM objects in $VM_Config is complete.
        Confirm-VM_Deployment -VM_Config_Obj $VM_Config -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials
 
        .NOTES
        the VM_Config_Obj object, should be like `
        `$VM_Config_Obj = @{}
        `$VM_01 = @{
            Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1'
            RAM = `$RAM
            CPU = `$CPUCount
            CM_Collection_Name = `$CM_Collection_W11_Autopilot
            Credentials = `$VM_Credentials
            DiskSize = `$DynamicDiskSize
            MAC = ""
        }
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [hashtable]
        $VM_Config_Obj,

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

        [Parameter(Mandatory = $true)]
        [PSCredential]
        $CM_Credentials
    )

    if ($null -eq $VM_Config_Obj) {
        throw "no VM_Config_Obj was supplied, please specify"
    }
    if ($null -eq $CM_Siteserver_FQDN) {
        throw "no CM_Siteserver_FQDN was supplied, please specify"
    }
    if ($null -eq $CM_Credentials) {
        throw "no CM_Credentials was supplied, please specify"
    }

    try {
        Write-Output "`nchecking vm os deployment status"
        Write-Output "------"
    
        Confirm-VMPresence -VM_Config_Obj $VM_Config_Obj    
        Invoke-Command -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials -ScriptBlock {
            $PSDriveName = "CM-PB2"

            Import-Module -Name ConfigurationManager
            New-PSDrive -Name $PSDriveName -PSProvider "CMSite" -Root $using:CM_Siteserver_FQDN -Description "Primary site" | Out-Null
            Set-Location -Path "$($PSDriveName):\"
    
            $Deployment_Finished = $false
            $Deployment_Check_Count = 0
            $CM_Deployment_Running = @{}
            foreach ($VM in $using:VM_Config_Obj.keys | Sort-Object) {
                $CM_Deployment_Running.Add($VM, $true)
            }
    
            do {
                $Deployment_Check_Count++
                $CM_All_Deployments = Get-CMDeploymentStatus | Get-CMDeploymentStatusDetails | `
                    Where-Object -FilterScript { $using:VM_Config_Obj.keys -contains $PSItem.Devicename } | `
                    Sort-Object -Property DeviceName
                Write-Output "timestamp : $(Get-Date -format "yyyy-MM-dd_HH.mm.ss") - $($Deployment_Check_Count)"
                if ($null -ne $CM_All_Deployments) {
                    foreach ($VM_Deployment in $CM_All_Deployments) {
                        if ($VM_Deployment.StatusDescription -notlike "*The task sequence manager successfully completed execution of the task sequence*") {
                            Write-Output "$($VM_Deployment.DeviceName): still running - $($VM_Deployment.StatusDescription)"
                            $CM_Deployment_Running.$($VM_Deployment.Devicename) = $true
                        }
                        else {
                            Write-Output "$($VM_Deployment.Devicename): finished - $($VM_Deployment.StatusDescription)"
                            $CM_Deployment_Running.$($VM_Deployment.Devicename) = $false
                        }
                    }
                }
                else {
                    Write-Output "Waiting on Deployments"
                }                
                Write-Output "------"
    
                if ($CM_Deployment_Running.Values -notcontains $true) {
                    $Deployment_Finished = $true
                }
                else {
                    Start-Sleep -Seconds 30
                }
    
            } while (($Deployment_Finished -eq $false) -and $Deployment_Check_Count -lt 180 )
    
            if ($Deployment_Check_Count -ge 200) {
                throw "deployment not finished after 100 mins"
            }

            Set-Location -Path $env:SystemDrive
            Remove-PSDrive -Name $PSDriveName
        }
        Write-Output "`nfinished vm deployments"
        Write-Output "------"
    }
    catch {
        throw "error while checking deployment status: $($PSItem.Exception.Message)"
    }    
}

function New-VMs_Objectbased {
    <#
        .Description
        Creates vms based on objects
 
        .Parameter VM_Config_Obj
        Object that contains multiple descriptive objects for deployment from a VM.
 
        .Parameter Course_Shortcut
        Name of the VM Switch
 
        .Parameter VMDriveLetter
        Letter for the volume where the vms should be stored
 
        .Example
        # Creates vms based on $VM_Config and connects them to the vm switch $Course_Shortcut
        New-VMs_Objectbased -VM_Config $VM_Config -Course_Shortcut $Course_Shortcut
 
        .NOTES
        edits $VM_Config by updating the macaddress attribute of each vm object
        the VM_Config_Obj object, should be like `
        `$VM_Config_Obj = @{}
        `$VM_01 = @{
            Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1'
            RAM = `$RAM
            CPU = `$CPUCount
            CM_Collection_Name = `$CM_Collection_W11_Autopilot
            Credentials = `$VM_Credentials
            DiskSize = `$DynamicDiskSize
            MAC = ""
        }
    #>


    param (
        [Parameter(Mandatory = $false)]
        [hashtable]
        $VM_Config_Obj,

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

        [Parameter(Mandatory = $false)]
        [char]
        $VMDriveLetter = 'V'
    )

    if ($null -eq $VM_Config_Obj) {
        throw "no VM_Config_Obj was supplied, please specify"
    }
    
    try {
        $VM_Base_Path = $VMDriveLetter + ":\VMs"
        Write-Output "`nstarting vm creation"
        Write-Output "------"
        foreach ($VM in $VM_Config_Obj.Keys | Sort-Object) {
            $VMVHDXPath = ($VM_Base_Path + "\" + $VM_Config_Obj.$VM.Name + "\" + $VM_Config_Obj.$VM.Name + ".vhdx")
            Write-Output "$($VM_Config_Obj.$VM.Name): creating vm"
            try {
                New-VHD -Path $VMVHDXPath -SizeBytes $VM_Config_Obj.$VM.DiskSize -Dynamic | Out-Null
                New-VM -Name $VM_Config_Obj.$VM.Name -MemoryStartupBytes $VM_Config_Obj.$VM.RAM -Path $VM_Base_Path -Generation 2 -VHDPath $VMVHDXPath -BootDevice NetworkAdapter -SwitchName $Course_Shortcut | Out-Null
            }
            catch {
                throw "$($VM_Config_Obj.$VM.Name): error during creation of vhdx or vm - $($PSItem.Exception.Message)"
            }
            try {
                Set-VMProcessor -VMName $VM_Config_Obj.$VM.Name -Count $VM_Config_Obj.$VM.CPU
                if ((Get-VM -Name $VM_Config_Obj.$VM.Name).DynamicMemoryEnabled) {
                    Set-VM -Name $VM_Config_Obj.$VM.Name -StaticMemory
                }

                Set-VMKeyProtector -VMName $VM_Config_Obj.$VM.Name -NewLocalKeyProtector
                Enable-VMTPM -VMName $VM_Config_Obj.$VM.Name
                Set-VM -AutomaticStartAction Start -VMName $VM_Config_Obj.$VM.Name -AutomaticStartDelay 10 
                Set-VM -AutomaticStopAction ShutDown -VMName $VM_Config_Obj.$VM.Name
                Get-VMIntegrationService -VMName $VM_Config_Obj.$VM.Name | Where-Object -Property Enabled -EQ $false | Enable-VMIntegrationService
            }
            catch {
                throw "$($VM_Config_Obj.$VM.Name): error while setting properties - $($PSItem.Exception.Message)"
            }
            Start-VM -Name $VM_Config_Obj.$VM.Name
            Start-Sleep -Seconds 2
            Stop-VM -Name $VM_Config_Obj.$VM.Name -Force -TurnOff
            Start-Sleep -Seconds 1
        }
        foreach ($VM in $VM_Config_Obj.Keys | Sort-Object) {
            $VM_Config_Obj.$VM.MAC = (Get-VM -Name $VM_Config_Obj.$VM.Name | Get-VMNetworkAdapter).MacAddress
            Set-VMNetworkAdapter -VMName $VM_Config_Obj.$VM.Name -StaticMacAddress $VM_Config_Obj.$VM.MAC
        }
        Confirm-VMPresence -VM_Config_Obj $VM_Config_Obj
    }
    catch {
        throw "error during creation of vms: $($PSItem.Exception.Message)"
    }    
}

function Test-VMConnection {
    <#
        .Description
        checks if a powershell direct connection to the vm can be established
 
        .Parameter VMId
        Id of the vm
 
        .Parameter LocalAdminCreds
        local admin credentials of the vm
 
        .Example
        # checks the powershell direct connection to VDC01
        Test-VMConnection -VMId (Get-VM -Name VDC01).Id -LocalAdminCreds $VM_Credentials
 
        .NOTES
         
    #>


    [cmdletbinding()]
    param (
        # VM object id
        [Parameter(Mandatory = $true)]
        [Guid]
        $VMId,

        # local admin credentials
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [PSCredential]
        $LocalAdminCreds 
    )

    $VM = Get-VM -Id $VMId
    try {
        Write-Output "------"
        if ($VM.State -eq "Off") {
            Write-Output "------"
            Write-Output "$($VM.Name): not running - starting"
            $VM | Start-VM -WarningAction SilentlyContinue
        }

        # Wait for the VM's heartbeat integration component to come up if it is enabled
        $HearbeatIC = (Get-VMIntegrationService -VM $VM | Where-Object Id -match "84EAAE65-2F2E-45F5-9BB5-0E857DC8EB47")
        if ($HearbeatIC -and ($HearbeatIC.Enabled -eq $true)) {
            $StartTime = Get-Date
            do {
                $WaitForMinitues = 5
                $TimeElapsed = $(Get-Date) - $StartTime
                if ($($TimeElapsed).TotalMinutes -ge 5) {
                    throw "$($VM.Name): integration components did not come up after $($WaitForMinitues) minutes"
                } 
                Start-Sleep -sec 1
            } 
            until ($HearbeatIC.PrimaryStatusDescription -eq "OK")
            Write-Output "$($VM.Name): heartbeat IC connected"
        }
        do {
            $WaitForMinitues = 5
            $TimeElapsed = $(Get-Date) - $StartTime
            Write-Output "$($VM.Name): testing connection"
            if ($($TimeElapsed).TotalMinutes -ge 5) {
                throw "$($VM.Name): could not connect to ps direct after $($WaitForMinitues) minutes"
            } 
            Start-Sleep -sec 3
            $PSReady = Invoke-Command -VMId $VMId -Credential $LocalAdminCreds -ErrorAction SilentlyContinue -ScriptBlock { $True } 
        } 
        until ($PSReady)
    }
    catch {
        throw "$($VM.Name): $($PSItem.Exception.Message)"
    }
    return $PSReady
}

function Confirm-VMPresence {
    <#
        .Description
        checks if vms are registered with the local hypervisor
 
        .Parameter VM_Config_Obj
        Object that contains multiple descriptive objects for deployment from a VM.
 
        .Example
        # checks if the vms in $VM_Config_Obj are registerd to the local hypervisor
        Confirm-VMPresence -VM_Config_Obj $VM_Config_Obj
 
        .NOTES
        the VM_Config_Obj object, should be like `
        `$VM_Config_Obj = @{}
        `$VM_01 = @{
            Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1'
            RAM = `$RAM
            CPU = `$CPUCount
            CM_Collection_Name = `$CM_Collection_W11_Autopilot
            Credentials = `$VM_Credentials
            DiskSize = `$DynamicDiskSize
            MAC = ""
        }
    #>


    param (
        [Parameter(Mandatory = $false)]
        [hashtable]
        $VM_Config_Obj
    )
    try {
        $VM_Config_Obj.Keys | ForEach-Object {
            if ($null -eq (Get-VM -Name $VM_Config_Obj.$PSItem.Name)) {
                throw "$($VM_Config_Obj.$PSItem.Name): could not be found"
            }
        }    
    }
    catch {
        throw "$($PSItem.Exception.Message)"
    }
}

function Get-SQLISO {
    <#
        .Description
        downloads sql iso from microsoft
 
        .Parameter Outpath
        path where the iso is stored
 
        .Example
        # stores the iso to $Outpath
        Get-SQLISO -Outpath $Outpath
 
        .NOTES
        downloads the sql enterprise eval setup and with it the iso
    #>


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

    if ((Test-Path -Path $Outpath) -eq $false) {
        New-Item -Path $Outpath -ItemType Directory -Force | Out-Null
    }
    $Outpath = (Get-Item -Path $Outpath).FullName
    $SQLEvalSetupPath = "SQL2019-SSEI-Eval.exe"
    $SQLEvalSetupFullPath = "$($Outpath)\$($SQLEvalSetupPath)"

    # downloading eval setup
    try {
        if ((Test-Path -Path $SQLEvalSetupFullPath) -eq $true) {
            if ((Get-Item $SQLEvalSetupFullPath).LastWriteTime -gt (Get-Date).AddHours(-2)) {
                # juenger als zwei stunden
                # do nothing
                Write-Output "$($env:COMPUTERNAME): found eval setup at $($SQLEvalSetupFullPath) will use it"
            } 
            else {
                Write-Output "$($env:COMPUTERNAME): found eval setup at $($SQLEvalSetupFullPath), removing the files because too old"
                Remove-Item -Path $SQLEvalSetupFullPath -Recurse -Force | Out-Null

                Write-Output "$($env:COMPUTERNAME): downloading eval setup"
                $URL = "https://download.microsoft.com/download/4/8/6/486005eb-7aa8-4128-aac0-6569782b37b0/SQL2019-SSEI-Eval.exe"
                New-Item -Path $Outpath -Force -ItemType Directory | Out-Null 
                Invoke-WebRequest -UseBasicParsing -Uri $URL -OutFile $SQLEvalSetupFullPath
            }            
        }
        else {
            Write-Output "$($env:COMPUTERNAME): downloading eval setup"
            $URL = "https://download.microsoft.com/download/4/8/6/486005eb-7aa8-4128-aac0-6569782b37b0/SQL2019-SSEI-Eval.exe"
            New-Item -Path $Outpath -Force -ItemType Directory | Out-Null 
            Invoke-WebRequest -UseBasicParsing -Uri $URL -OutFile $SQLEvalSetupFullPath
        }
    }
    catch {
        throw "error downloading eval setup: $($PSItem.Exception.Message)"
    }

    # downloading iso
    try {
        $ISOPath = (Get-ChildItem -Path $Outpath | Where-Object -FilterScript { $PSItem.Name -like "SQL*.iso" }).FullName
        if ($null -ne $ISOPath -and (Test-Path -Path $ISOPath) -eq $true) {
            if ((Get-Item $ISOPath).LastWriteTime -gt (Get-Date).AddHours(-2)) {
                # juenger als zwei stunden
                # do nothing
                Write-Output "$($env:COMPUTERNAME): found sql iso at $($ISOPath) will use it"
            } 
            else {
                Write-Output "$($env:COMPUTERNAME): found sql iso at $($ISOPath), removing the files because too old"
                Remove-Item -Path $ISOPath -Recurse -Force | Out-Null

                Write-Output "$($env:COMPUTERNAME): downloading sql iso with eval setup"
                $Arguments = "/ACTION=Download /MEDIAPATH=$($Outpath)\ /MEDIATYPE=ISO /LANGUAGE=en-US /QUIET"
                Start-Process $SQLEvalSetupFullPath -ArgumentList $Arguments -Wait -NoNewWindow
            }            
        }
        else {
            Write-Output "$($env:COMPUTERNAME): downloading sql iso with eval setup"
            $Arguments = "/ACTION=Download /MEDIAPATH=$($Outpath)\ /MEDIATYPE=ISO /LANGUAGE=en-US /QUIET"
            Start-Process $SQLEvalSetupFullPath -ArgumentList $Arguments -Wait -NoNewWindow
        }
    }
    catch {
        throw "error downloading iso: $($PSItem.Exception.Message)"
    }
    Write-Output "$($env:COMPUTERNAME): finished download - check folder '$($Outpath)'"
}

function Initialize-SQLSetup {
    <#
        .Description
        prepares the sql installation files for i
 
        .Parameter Outpath
        path where the data should be cached
 
        .Example
        # stores the files to $TempFolderForSQL
        Initialize-SQLSetup -Outpath $TempFolderForSQL
 
        .NOTES
        downloads the sql install files and stores them extracted to the outpath
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Outpath
    )
    
    if ((Test-Path -Path $Outpath) -eq $false) {
        New-Item -Path $Outpath -ItemType Directory -Force | Out-Null
    }
    $Outpath = (Get-Item -Path $Outpath).FullName

    # checks
    $SQLFileFolder = "$($Outpath)\SQLFiles\"
    if ((Test-Path -Path $SQLFileFolder) -eq $true ) {
        throw "$($env:COMPUTERNAME): folder '$($SQLFileFolder)' already exits, please cleanup or skip"
    }

    # downloading iso
    try {
        Get-SQLISO -Outpath $Outpath
    }
    catch {
        throw "error during download: $($PSItem.Exception.Message)"
    }
    
    # copying files
    try {
        if ((Test-Path -Path $SQLFileFolder) -eq $true) {
            if ((Get-Item $SQLFileFolder).LastWriteTime -gt (Get-Date).AddHours(-2)) {
                # juenger als zwei stunden
                # do nothing
                Write-Output "$($env:COMPUTERNAME): found setup files at $($SQLFileFolder) will use it"
            } 
            else {
                Write-Output "$($env:COMPUTERNAME): found setup files at $($SQLFileFolder), removing the files because too old"
                Remove-Item -Path $SQLFileFolder -Recurse -Force | Out-Null

                Write-Output "$($env:COMPUTERNAME): mounting iso"
                $ISO = Get-ChildItem -Path $Outpath | Where-Object -FilterScript { $PSItem.Name -like "SQL*.iso" }
                $MountedISOs = Mount-DiskImage -PassThru -ImagePath $ISO.FullName
                $Volume = Get-Volume -DiskImage $MountedISOs
                Write-Output "$($env:COMPUTERNAME): file '$($ISO.FullName)' mounted to '$($Volume.DriveLetter):\'"
                Write-Output "$($env:COMPUTERNAME): copy files from iso to $($SQLFileFolder)"
                Copy-Item -Path "$($Volume.DriveLetter):\" -Destination $SQLFileFolder -Recurse -Force
                Write-Output "$($env:COMPUTERNAME): finished copy job"
                Write-Output "$($env:COMPUTERNAME): dismounting iso"
                Dismount-DiskImage $MountedISOs.ImagePath | Out-Null
                Write-Output "$($env:COMPUTERNAME): dismounting finished"
            }            
        }
        else {
            Write-Output "$($env:COMPUTERNAME): mounting iso"
            $ISO = Get-ChildItem -Path $Outpath | Where-Object -FilterScript { $PSItem.Name -like "SQL*.iso" }
            $MountedISOs = Mount-DiskImage -PassThru -ImagePath $ISO.FullName
            $Volume = Get-Volume -DiskImage $MountedISOs
            Write-Output "$($env:COMPUTERNAME): file '$($ISO.FullName)' mounted to '$($Volume.DriveLetter):\'"
            Write-Output "$($env:COMPUTERNAME): copy files from iso to $($SQLFileFolder)"
            Copy-Item -Path "$($Volume.DriveLetter):\" -Destination $SQLFileFolder -Recurse -Force
            Write-Output "$($env:COMPUTERNAME): finished copy job"
            Write-Output "$($env:COMPUTERNAME): dismounting iso"
            Dismount-DiskImage $MountedISOs.ImagePath | Out-Null
            Write-Output "$($env:COMPUTERNAME): dismounting finished"
        }
    }
    catch {
        throw "error during mount or copy: $($PSItem.Exception.Message)"
    }
}

function Install-SQLADServiceAccount {
    <#
        .Description
        adds the local server to a group with permission to the serviec account and then installs it
 
        .Parameter SQLServiceAccount
        name of the group managed service account
 
        .Parameter GroupWithPermissions
        name of the group with permissions to retrieve the password of the group managed service account
 
        .Example
        # this installs a gMSA
        Install-SQLADServiceAccount -SQLServiceAccount $SQLEngine.Name -GroupWithPermissions $SQLEngine.GroupWithPermissions
 
        .NOTES
        - the server needs to be member of a domain
        - the group managed service account must exit
        - the user which runs this command must have permissions to add the local device to the group
    #>


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

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

    if ((Get-CimInstance -ClassName Win32_ComputerSystem).PartofDomain -eq $false) {
        throw "$($env:COMPUTERNAME): not a member of a domain"
    }

    Install-WindowsFeature -Name RSAT-AD-PowerShell | Out-Null

    if ($null -eq (Get-ADServiceAccount -Identity $SQLServiceAccount -ErrorAction SilentlyContinue)) {
        throw "service account '$($SQLServiceAccount)$' could not be found"
    }

    # adding device to group
    try {
        Write-Output "$($env:COMPUTERNAME): adding to group '$($GroupWithPermissions)'"
        $Self = Get-ADComputer -Identity $env:COMPUTERNAME
        Add-ADGroupMember -Identity $GroupWithPermissions -Members $Self
    }
    catch {
        throw "not able to add to group '$($GroupWithPermissions): $($PSItem.Exception.Message)"
    }
    
    # installing and testing service account
    try {
        Write-Output "$($env:COMPUTERNAME): installing '$($SQLServiceAccount)'"
        Start-Process -FilePath klist -ArgumentList "purge -lh 0 -li 0x3e7" -NoNewWindow
        Start-Sleep -Seconds 5
        Install-ADServiceAccount -Identity ($SQLServiceAccount + "$")
        if ((Test-ADServiceAccount -Identity ($SQLServiceAccount + "$") -WarningAction SilentlyContinue) -eq $false) {
            throw "service account '$($SQLServiceAccount)$' is not installed, please verify"
        }
    }
    catch {
        throw "could not install '$($SQLServiceAccount)' - $($PSItem.Exception.Message)"
    }
}

function Install-SQLInstance {
    <#
        .Description
        adds the local server to a group with permission to the serviec account and then installs it
 
        .Parameter Name
        name of group managed service account
         
        .Parameter UseLocalAccount
        specify if local accounts are used
 
        .Parameter UseGmSA
        specify if group managed service accounts are used
         
        .Parameter EngineAccountName
        service account name for sql engine
 
        .Parameter AgentAccountName
        service account name for sql agent
 
        .Parameter UseMixedAuth
        when used, the instance will use sql and windows authentication
 
        .Parameter SAPWD
        sa pwd
         
        .Parameter INSTALLSQLDATADIR
        specifies the data directory for SQL Server data files
         
        .Parameter INSTANCEDIR
        specifies nondefault installation directory for instance-specific components
         
        .Parameter SQLBACKUPDIR
        backup file path for the sql database
         
        .Parameter SQLUSERDBDIR
        user file path for the sql database
 
        .Parameter SQLUSERDBLOGDIR
        user log file path for the sql database
         
        .Parameter SQLTEMPDBDIR
        temp file path for the sql database
         
        .Parameter SQLTEMPDBLOGDIR
        temp log file path for the sql database
 
        .Parameter Features
        features that should be installed, like 'SQLENGINE,FULLTEXT,CONN'
         
        .Parameter SQLSYSADMINACCOUNTS
        accounts that should be admin on that instance
 
        .Parameter SQLMinRAM
        minimum ram for instance
 
        .Parameter SQLMaxRAM
        maximum ram for instance
 
        .Example
        # this installs an sql instance with the name PB1 which uses gmsa with customized file paths
        Install-SQLInstance -Name "PB1" `
            -Features "SQLENGINE" `
            -SQLSYSADMINACCOUNTS ('"' + "IC\T1-CMAdmin" + '" "' + "IC\T1-CMAdmins" + '"') `
            -SQLBACKUPDIR "S:\SQL\PB1\BACKUP\DATA" `
            -SQLUSERDBDIR "S:\SQL\PB1\USERDATA\DATA" `
            -SQLUSERDBLOGDIR "S:\SQL\PB1\USERLOG\LOG" `
            -SQLTEMPDBDIR "S:\SQL\PB1\TEMPDATA\DATA" `
            -SQLTEMPDBLOGDIR "S:\SQL\PB1\TEMPLOG\LOG" `
            -UseGmSA `
            -EngineAccountName $SQLEngine `
            -AgentAccountName $SQLAgent
 
        .Example
        # this installs an sql instance with the name INSTANCE and local accounts to default file paths
        Install-SQLInstance -Name "INSTANCE" -UseLocalAccount
 
        .NOTES
         
    #>


    [CmdletBinding(DefaultParameterSetName = 'local')]
    param (
        [Parameter(ParameterSetName = 'local', Mandatory = $false)]
        [Parameter(ParameterSetName = 'gmsa', Mandatory = $false)]
        [string]
        $SetupPath = ".\SQLFiles\setup.exe",

        [Parameter(ParameterSetName = 'local', Mandatory = $true)]
        [Parameter(ParameterSetName = 'gmsa', Mandatory = $true)]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'local')]
        [switch]
        $UseLocalAccount,

        [Parameter(ParameterSetName = 'gmsa')]
        [switch]
        $UseGmSA,

        [Parameter(ParameterSetName = 'gmsa', Mandatory = $true)]
        [string]
        $EngineAccountName,

        [Parameter(ParameterSetName = 'gmsa', Mandatory = $true)]
        [string]
        $AgentAccountName,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [switch]
        $UseMixedAuth,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SAPWD,

        [Parameter(ParameterSetName = 'local', Mandatory = $false)]
        [Parameter(ParameterSetName = 'gmsa', Mandatory = $false)]
        [string]
        $INSTALLSQLDATADIR,
        
        [Parameter(ParameterSetName = 'local', Mandatory = $false)]
        [Parameter(ParameterSetName = 'gmsa', Mandatory = $false)]
        [string]
        $INSTANCEDIR,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLBACKUPDIR,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLUSERDBDIR,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLUSERDBLOGDIR,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLTEMPDBDIR,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLTEMPDBLOGDIR,

        [Parameter(ParameterSetName = 'local', Mandatory = $false)]
        [Parameter(ParameterSetName = 'gmsa', Mandatory = $false)]
        [string]
        $Features,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLSYSADMINACCOUNTS,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLMinRAM,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLMaxRAM
    )

    # define arguments
    if ($UseLocalAccount -eq $false -and $UseGmSA -eq $false) {
        throw "you have to specify 'UseLocalAccount' or 'UseGmSA'"
    }
    if ($UseMixedAuth -eq $false -and $null -eq $SAPWD) {
        throw "you have to specify 'SAPWD' when 'UseMixedAuth' is used"
    }
    if ($uselocalaccount) {
        $AGTSVCACCOUNT = 'NT Service\SQLAgent$' + $Name
        $SQLSVCACCOUNT = 'NT Service\MSSQL$' + $Name
    }
    elseif ($UseGmSA) {
        $AGTSVCACCOUNT = $env:USERDOMAIN + "\" + (Get-ADServiceAccount -Identity $AgentAccountName).SamAccountName
        $SQLSVCACCOUNT = $env:USERDOMAIN + "\" + (Get-ADServiceAccount -Identity $EngineAccountName).SamAccountName
    }
    $SQLTELSVCACCT = 'NT Service\SQLTELEMETRY$' + $Name
    if ($Features -eq "") {
        $Features = "SQLENGINE"
    }
    if ($null -eq $SQLBACKUPDIR) {
        $SQLBACKUPDIR = $INSTALLSQLDATADIR + "\BACKUP\DATA"
    }
    if ($null -eq $SQLUSERDBDIR) {
        $SQLUSERDBDIR = $INSTALLSQLDATADIR + "\USER\DATA"
    }
    if ($null -eq $SQLUSERDBLOGDIR) {
        $SQLUSERDBLOGDIR = $INSTALLSQLDATADIR + "\USERLOG\LOG"
    }
    if ($null -eq $SQLTEMPDBDIR) {
        $SQLTEMPDBDIR = $INSTALLSQLDATADIR + "\TEMP\DATA"
    }
    if ($null -eq $SQLTEMPDBLOGDIR) {
        $SQLTEMPDBLOGDIR = $INSTALLSQLDATADIR + "\TEMPLOG\LOG"
    }
    if ($null -eq $SQLSYSADMINACCOUNTS) {
        $SQLSYSADMINACCOUNTS = ('"' + "$($env:COMPUTERNAME)\Administrator" + '"')
    }

    # check dependencies
    try {
        if ($UseGmSA) {
            if ($env:USERDOMAIN -eq $env:COMPUTERNAME) {
                throw "you are logged in with a local user, domain user required"
            }
            if ((Test-ADServiceAccount -Identity "$($EngineAccountName)$") -eq $false) {
                throw "gmsa '$($EngineAccountName)' not installed"
            }
            if ((Test-ADServiceAccount -Identity "$($AgentAccountName)$") -eq $false) {
                throw "gmsa '$($AgentAccountName)' not installed"
            }
        }
        $InstalledInstances = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server' -ErrorAction SilentlyContinue).InstalledInstances
        if ($InstalledInstances -contains $Name) {
            throw "SQL instance is already installed '$($Name)'"
        }
    }
    catch {
        throw "error during prerequesits check : $($Psitem.Exception.Message)"
    }

    # start installation
    try {
        Write-Output "$($env:COMPUTERNAME): starting install of '$($Name)'"
        $Arguments = @(
            '/IACCEPTSQLSERVERLICENSETERMS="True"'
            '/IACCEPTPYTHONLICENSETERMS="False"'
            '/ACTION="Install"'
            '/ENU="True"'
            '/IACCEPTROPENLICENSETERMS="False"'
            '/SUPPRESSPRIVACYSTATEMENTNOTICE="False"'
            '/QUIET="True"'
            '/QUIETSIMPLE="False"'
            '/UpdateEnabled="True"'
            '/USEMICROSOFTUPDATE="False"'
            '/SUPPRESSPAIDEDITIONNOTICE="False"'
            '/UpdateSource="MU"'
            ('/FEATURES=' + $Features)
            ('/INSTANCENAME="' + $Name + '"')
            '/INSTALLSHAREDDIR="C:\Program Files\Microsoft SQL Server"'
            '/INSTALLSHAREDWOWDIR="C:\Program Files (x86)\Microsoft SQL Server"'
            ('/INSTANCEID="' + $Name + '"')
            ('/SQLTELSVCACCT="' + $SQLTELSVCACCT + '"')
            '/SQLTELSVCSTARTUPTYPE="Automatic"'            
            ('/AGTSVCACCOUNT="' + $AGTSVCACCOUNT + '"')
            '/AGTSVCSTARTUPTYPE="Automatic"'
            '/SQLSVCSTARTUPTYPE="Automatic"'
            '/SQLCOLLATION="SQL_Latin1_General_CP1_CI_AS"'
            ('/SQLSVCACCOUNT="' + $SQLSVCACCOUNT + '"')
            ('/SQLSYSADMINACCOUNTS=' + $SQLSYSADMINACCOUNTS)
            ('/SQLBACKUPDIR="' + $SQLBACKUPDIR + '"')
            ('/SQLUSERDBDIR="' + $SQLUSERDBDIR + '"')
            ('/SQLUSERDBLOGDIR="' + $SQLUSERDBLOGDIR + '"')
            ('/SQLTEMPDBDIR="' + $SQLTEMPDBDIR + '"')
            ('/SQLTEMPDBLOGDIR="' + $SQLTEMPDBLOGDIR + '"')
            '/TCPENABLED="1"'
            '/NPENABLED="0"'
            '/BROWSERSVCSTARTUPTYPE="Automatic"'
        )
        if ($UseMixedAuth -eq $true) {
            $SAArguments = @(
                '/SECURITYMODE="SQL"'
                '/SAPWD="' + $SAPWD + '"'
            )
            $Arguments = $Arguments + $SAArguments
        }
        if ($INSTALLSQLDATADIR -ne "") {
            $INSTALLSQLDATADIR_Arguments = @(
                ('/INSTALLSQLDATADIR="' + $INSTALLSQLDATADIR + '"')
            )
            $Arguments = $Arguments + $INSTALLSQLDATADIR_Arguments
        }
        if ($INSTANCEDIR -ne "") {
            $INSTANCEDIR_Arguments = @(
                ('/INSTANCEDIR="' + $INSTANCEDIR + '"')
            )
            $Arguments = $Arguments + $INSTANCEDIR_Arguments
        }
        if ($SQLMinRAM -ne "") {
            $MinRAMArguments = @(
                ('/SQLMINMEMORY="' + $SQLMinRAM + '"')
            )
            $Arguments = $Arguments + $MinRAMArguments
        }
        if ($SQLMaxRAM -ne "") {
            $MaxRAMArguments = @(
                ('/SQLMAXMEMORY="' + $SQLMaxRAM + '"')
            )
            $Arguments = $Arguments + $MaxRAMArguments
        }

        $Process = Start-Process $SetupPath -ArgumentList $Arguments -Wait -NoNewWindow -PassThru
        if ($Process.ExitCode -ne 0) {
            throw "check logs of sql setup - $($env:ProgramFiles)\Microsoft SQL Server\*\Setup Bootstrap\Log"
        }
    }
    catch {
        throw "error during installation of sql instance '$($Name): $($Psitem.EXception.Message)"
    }
    # service customization
    try {
        if ($UseGmSA) {
            Write-Output "$($env:COMPUTERNAME): configuring SQLAgent`$$($Name) service to autostart delayed"
            $Agent = (Get-Service -Name "SQLAgent`$$($Name)").Name
            Start-Process "sc" -ArgumentList ('config "' + $($Agent) + '" start=delayed-auto') -NoNewWindow

            Write-Output "$($env:COMPUTERNAME): configuring MSSQL`$$($Name) service to autostart delayed"
            $Engine = (Get-Service -Name "MSSQL`$$($Name)").Name    
            Start-Process "sc" -ArgumentList ('config "' + $($Engine) + '" start=delayed-auto') -NoNewWindow
        }
    }
    catch {
        throw "error during service customization : $($Psitem.EXception.Message)"
    }
    Write-Output "$($env:COMPUTERNAME): finished install of '$($Name)'"
}

function Install-SQLCU {
    <#
        .Description
        installs sql 2019 latest cu to all local instances
 
        .Example
        # this installes sql 2019 latest cu to all instances
        Install-SQLCU
 
        .NOTES
 
    #>


    try {
        $SQLVersion = "2019"
        $TempFolderForSQL = "C:\ProgramData\NTS\SQL\CU"
        $SQLCUFilePath = "$($TempFolderForSQL)\SQL2019-latest-cu.exe"
        $SQLCUUnpackedFilePath = "$($TempFolderForSQL)\SQL2019-latest-cu\"
        
        if ((Test-Path -Path $TempFolderForSQL) -eq $false) {
            New-Item -Path $TempFolderForSQL -ItemType Directory -Force | Out-Null
        }

        # link to latest cu of sql 2019 https://www.microsoft.com/en-us/download/confirmation.aspx?id=100809
        $Content = Invoke-WebRequest -UseBasicParsing -Uri "https://www.microsoft.com/en-us/download/confirmation.aspx?id=100809"
        $UpdateLink = ($Content.Links | Where-Object -FilterScript { $PSItem.href -like "*download.microsoft.com*" -and $PSItem.outerHTML -like "*download manually*" }).href
        
        Write-Output "$($env:COMPUTERNAME): downloading sql cu"
        Invoke-WebRequest -UseBasicParsing -Uri $UpdateLink -OutFile $SQLCUFilePath
        
        Write-Output "$($env:COMPUTERNAME): unpacking sql cu"
        $Arguments = "/X:$($SQLCUUnpackedFilePath)"
        $Process = Start-Process $SQLCUFilePath -ArgumentList $Arguments -Wait -NoNewWindow -PassThru
        if ($Process.ExitCode -ne 0) {
            throw "there was an error unpacking cu files"
        }
    
        Write-Output "$($env:COMPUTERNAME): installing sql cu"
        $Arguments = "/ACTION=Patch /ALLINSTANCES /QUIET /IACCEPTSQLSERVERLICENSETERMS /ENU"
        $Process = Start-Process ($SQLCUUnpackedFilePath + "setup.exe") -ArgumentList $Arguments -Wait -NoNewWindow -PassThru
        if ($Process.ExitCode -ne 0) {
            if ($SQLVersion -eq "2019") {
                $Content = Get-Content -Path "C:\Program Files\Microsoft SQL Server\150\Setup Bootstrap\Log\Summary.txt"
            }
            $SetupExitMessage = $Content.split("`n")[5].Replace(" Exit message: ","")
            if ($SetupExitMessage -like "*No features were updated during the setup execution. The requested features may not be installed or features are already at a higher patch level*") {
                Write-Output "$($env:COMPUTERNAME): no updates were applied, because already at a higher or equal patch level"
            }
            else {
                throw "there was an error installing sql cu - $($SetupExitMessage)"
            }
        }
    
        Write-Output "$($env:COMPUTERNAME): removing temp files"
        try {
            Remove-Item -Path $TempFolderForSQL -Recurse -Force
        }
        catch {
            throw "error while cleanup - $($PSItem.Exception.Message)"
        }
    }
    catch {
        Write-Output "$($env:COMPUTERNAME): removing temp files"
        try {
            Remove-Item -Path $TempFolderForSQL -Recurse -Force
        }
        catch {
            throw "error while cleanup - $($PSItem.Exception.Message)"
        }
        throw "$($env:COMPUTERNAME): error sql cu installation - $($PSItem.Exception.Message) - see logs at C:\Program Files\Microsoft SQL Server\*\Setup Bootstrap\Log\"
    }    
}

function Add-VMDisk {
    <#
        .Description
        add a virtual disk to a vm
 
        .Parameter VMName
        name of the vm
 
        .Parameter VHDXPath
        where should the vhdx file be stored
 
        .Parameter VHDXSize
        size in byte of the disk
 
        .Example
        # this will add a disk to the vm and store ist at the file location from the vm
        Add-VMDisk -VMName $VM.Name -VHDXPath ($VM.ConfigurationLocation + "\" + $VM.Name + "-2.vhdx") -VHDXSize $DMP_ContentLibDisk_Size
 
        .NOTES
         
    #>


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

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

        [Parameter(Mandatory = $false)]
        [string]
        $VHDXSize = 80GB
    )

    try {
        Write-Host "$($VMName): creating disk '$($VHDXPath)'"
        New-VHD -Path $VHDXPath  -SizeBytes $VHDXSize -Dynamic | Out-Null
        Add-VMHardDiskDrive -VMName $VMName -Path $VHDXPath 
    }
    catch {
        throw "$($PSItem.Exception.Message)"
    }
}

function Set-VMIPConfig {
    <#
        .Description
        configures the network interface of a vm
 
        .Parameter VMName
        name of the vm
 
        .Parameter VMCredential
        local admin credentials of the vm
 
        .Parameter IPAddress
        ip address that should be assigned
 
        .Parameter NetPrefix
        subnet prefix, aka 24 or 16
 
        .Parameter DefaultGateway
        gateway of the subnet
 
        .Parameter DNSAddresses
        dns server ip addresses
 
        .Example
        # this will configure the ip interface
        Set-VMIPConfig -VMName $DMP_VM.Name -VMCredential $VMCredential-IPAddress "192.168.1.21" -NetPrefix $NetPrefix -DefaultGateway $DefaultGateway -DNSAddresses $DNSAddresses
 
        .NOTES
        always uses the first nic founc on the system
    #>


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

        [Parameter(Mandatory = $true)]
        [pscredential]
        $VMCredential,

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

        [Parameter(Mandatory = $true)]
        [int]
        $NetPrefix,

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

        [Parameter(Mandatory = $true)]
        [string[]]
        $DNSAddresses
    )
    
    try {
        Write-Host "$($VMName): configuring network"
        Invoke-Command -VMName $VMName -Credential $VMCredential -ScriptBlock {
            $InterfaceObject = (Get-NetAdapter)[0]
            Write-Host "$($env:COMPUTERNAME): nic with mac '$($InterfaceObject.MacAddress)' was selected"
            If (($InterfaceObject | Get-NetIPConfiguration).IPv4Address.IPAddress) {
                $InterfaceObject | Remove-NetIPAddress -AddressFamily "IPv4" -Confirm:$false
            }
            If (($InterfaceObject | Get-NetIPConfiguration).Ipv4DefaultGateway) {
                $InterfaceObject | Remove-NetRoute -AddressFamily "IPv4" -Confirm:$false
            }
            Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\services\Tcpip\Parameters\Interfaces\$($InterfaceObject.InterfaceGuid)" -Name EnableDHCP -Value 0
            Start-Sleep -Seconds 2
    
            Write-Host "$($env:COMPUTERNAME): nic with mac '$($InterfaceObject.MacAddress)' will have ip '$($using:IPAddress)'"
            $InterfaceObject | New-NetIPAddress -IPAddress $using:IPAddress -AddressFamily "IPv4" -PrefixLength $using:NetPrefix -DefaultGateway $using:DefaultGateway | Out-Null
            $InterfaceObject | Set-DnsClientServerAddress -ServerAddresses $using:DNSAddresses
        }
    }
    catch {
        throw "$($VMName): error setting ip interface - $($PSItem.Exception.Message)"
    }
}

function Add-VMToDomain {
    <#
        .Description
        takes the vm into a domain
 
        .Parameter VMName
        name of the vm
 
        .Parameter VMCredential
        local admin credentials of the vm
 
        .Parameter DomainName
        name of the domain where the vm should be joined
 
        .Parameter DomainCredential
        domain credentials with the permission to join devices
 
        .Parameter OUPath
        ou path in the domain where the vm should be organized
 
        .Parameter NoReboot
        when used, the vm will not reboot after join
 
        .Example
        # this will join the vm to the specified domain and OU
        Add-VMToDomain -VMName $SiteServer_VM.Name -VMCredential $VMCredential -DomainName $DomainName -DomainCredential $Domain_Credentials -OUPath "OU=Servers,OU=CM,OU=TIER-1,OU=ESAE,DC=INTUNE-CENTER,DC=DE"
 
        .NOTES
         
    #>


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

        [Parameter(Mandatory = $true)]
        [pscredential]
        $VMCredential,

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

        [Parameter(Mandatory = $true)]
        [pscredential]
        $DomainCredential,        

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

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

    try {
        Confirm-VMState -VMObject (Get-VM -Name $VMName) -VMCredential $VMCredential
        Write-Output "$($VMName): joining domain '$($DomainName)'"
        Invoke-Command -VMName $VMName -Credential $VMCredential -ScriptBlock { 
            Start-Sleep -Seconds 1
            if ($NoReboot -eq $true) {
                if ($null -ne $OUPath) {
                    Add-Computer -Credential $using:DomainCredential -DomainName $using:DomainName -OUPath $using:OUPath -WarningAction SilentlyContinue
                }
                else {
                    Add-Computer -Credential $using:DomainCredential -DomainName $using:DomainName -WarningAction SilentlyContinue
                }
            }
            else {
                if ($null -ne $OUPath) {
                    Add-Computer -Credential $using:DomainCredential -DomainName $using:DomainName -OUPath $using:OUPath -Restart -WarningAction SilentlyContinue
                }
                else {
                    Add-Computer -Credential $using:DomainCredential -DomainName $using:DomainName -Restart -WarningAction SilentlyContinue
                }
            }
        }
        if ($NoReboot -eq $true) {
            Write-Output "$($VMName): domainjoin succesful - vm reboot required"
        }
        else {
            Write-Output "$($VMName): domainjoin succesful - vm will do reboot"
        }        
    }
    catch {
        throw "$($VMName): error joining to domain '$($DomainName)' - $($PSItem.Exception.Message)"
    }
}

function Confirm-VMState {
    <#
        .Description
        checks if the vm is running and starts it if necessary then checks the connection to the vm via powershell direct
 
        .Parameter VMObject
        name of the vm
 
        .Parameter VMCredential
        local admin credentials of the vm
 
        .Example
        # this will check the specified vm
        Confirm-VMState -VMObject $VM -VMCredential $VMCred
 
        .NOTES
        VMObject should be like this:
        $VM_01 = @{
            Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1'
            RAM = `$RAM
            CPU = `$CPUCount
            CM_Collection_Name = `$CM_Collection_W11_Autopilot
            Credentials = `$VM_Credentials
            DiskSize = `$DynamicDiskSize
            MAC = ""
    #>


    param (
        # VM Objects
        [Parameter(Mandatory = $true)]
        [System.Object]
        $VMObject,

        [Parameter(Mandatory = $true)]
        [PSCredential]
        $VMCredential
    )
    try {
        if ($VMObject.State -ne "Running") {
            Start-VM -VM $VMObject
            Start-Sleep -Seconds 10
        }
        if (Test-VMConnection -VMId $VMObject.Id -LocalAdminCreds $VMCredential) {
            # Write-Output "$($VMObject.Name): connected succesfully"
        }
        else {
            throw "$($VMObject.Name): error while connecting with ps direct - $($PSItem.Exception.Message)"
        }    
    }
    catch {
        throw "$($PSItem.Exception.Message)"
    }
}

function Install-NLASvcFixForDCs {
    <#
        .Description
        checks if dns is specified as dependy for service nlasvc and if its not the case it will add it.
 
        .Example
        # this will service dns as dependency for the service nlasvc
        Install-NLASvcFixForDCs
 
        .NOTES
         
    #>


    try {
        $RegKey = "HKLM:\SYSTEM\CurrentControlSet\Services\NlaSvc"
        $RegKeyValue = "DependOnService"
        $ServiceName = "DNS"

        Write-Output "$($env:COMPUTERNAME): adding service $($ServiceName) as dependency for service NlaSvc"
        $NLASvcDependencies = (Get-ItemProperty -Path $RegKey -Name $RegKeyValue).$RegKeyValue
        if ($NLASvcDependencies -notcontains $ServiceName) {
            $FixedNLASvcDependencies = $NLASvcDependencies + $ServiceName
            Set-ItemProperty -Path $RegKey -Name $RegKeyValue -Value $FixedNLASvcDependencies
            Write-Output "$($env:COMPUTERNAME): NlaSvc fix applied"
        }
        else {
            Write-Output "$($env:COMPUTERNAME): NlaSvc fix already applied"
        }
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function New-EASEOUStructure {
    <#
        .Description
        this will create the esae structure with OUs, User, Groups and Group Managed Service Accounts
 
        .Example
        # creates the ease structure on the local domain controller
        New-EASEOUStructure
 
        .NOTES
        must be executed on a domain controller
         
    #>


    if ((Get-CimInstance -ClassName Win32_OperatingSystem).ProductType -ne 2) {
        throw "$($env:COMPUTERNAME): this is not a domain controller, can only be used on domain controllers"
    }
    
    Import-Module Activedirectory
    $Domain = Get-ADDomain

    Write-Output "$($env:COMPUTERNAME): apply esae structure on $($Domain.Forest)"
    
    #region ous
    try {
        Write-Output "$($env:COMPUTERNAME): creating ous"
        $TopLevelOU = 'ESAE'
        New-ADOrganizationalUnit -Name $TopLevelOU -Path $Domain.DistinguishedName
        
        # ESAE
        $ESAEPath = "OU=$TopLevelOU,$($Domain.DistinguishedName)"
        $ESAEOUs = 'Groups', 'PAW', 'Tier 0', 'Tier 1', 'Tier 2' 
        $ESAEOUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $ESAEPath
        }
            
        # ESAE\PAW
        $PAWPath = "OU=PAW,$ESAEPath"
        $PAWOUs = 'Accounts', 'Devices', 'Groups' 
        $PAWOUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $PAWPath 
        }
            
        # ESAE\Tier0
        $Tier0Path = "OU=Tier 0,$ESAEPath"
        $Tier0OUs = 'AD', 'PKI', 'SQL', 'ADFS', 'ADSync'
        $Tier0OUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $Tier0Path 
        }
            
        # ESAE\Tier1
        $Tier1Path = "OU=Tier 1,$ESAEPath"
        $Tier1OUs = 'Exchange', 'ConfigMgr', 'SQL'
        $Tier1OUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $Tier1Path
        }
            
        # ESAE\Tier2
        $Tier2Path = "OU=Tier 2,$ESAEPath"
        $Tier2OUs = 'Accounts', 'Devices', 'Groups'
        $Tier2OUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $Tier2Path
        }
            
        # ESAE\Tier0\AD
        $Tier0ADPath = "OU=AD,$Tier0Path"
        $Tier0ADOUs = 'Accounts', 'Groups' 
        $Tier0ADOUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $Tier0ADPath
        }
            
        # ESAE\Tier0\PKI
        $Tier0PKIPath = "OU=PKI,$Tier0Path"
        $Tier0PKIOUs = 'Accounts', 'Groups', 'Servers'
        $Tier0PKIOUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $Tier0PKIPath 
        }
        
        # ESAE\Tier0\SQL
        $Tier0SQLPath = "OU=SQL,$Tier0Path"
        $Tier0SQLOUs = 'Accounts', 'Groups', 'Servers'
        $Tier0SQLOUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $Tier0SQLPath 
        }
    
        # ESAE\Tier0\ADFS
        $Tier0ADFSPath = "OU=ADFS,$Tier0Path"
        $Tier0ADFSOUs = 'Accounts', 'Groups', 'Servers'
        $Tier0ADFSOUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $Tier0ADFSPath 
        }
    
        # ESAE\Tier0\ADSync
        $Tier0ADSyncPath = "OU=ADSync,$Tier0Path"
        $Tier0ADSyncOUs = 'Accounts', 'Groups', 'Servers'
        $Tier0ADSyncOUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $Tier0ADSyncPath 
        }
    
        # ESAE\Tier1\Exchange
        $Tier1ExchangePath = "OU=Exchange,$Tier1Path"
        $Tier1ExchangeOUs = 'Accounts', 'Groups', 'Servers'
        $Tier1ExchangeOUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $Tier1ExchangePath 
        }
            
        # ESAE\Tier1\ConfigMgr
        $Tier1ConfigMgrPath = "OU=ConfigMgr,$Tier1Path"
        $Tier1ConfigMgrOUs = 'Accounts', 'Groups', 'Servers'
        $Tier1ConfigMgrOUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $Tier1ConfigMgrPath 
        }
            
        # ESAE\Tier1\SQL
        $Tier1SQLPath = "OU=SQL,$Tier1Path"
        $Tier1SQLOUs = 'Accounts', 'Groups', 'Servers'
        $Tier1SQLOUs | ForEach-Object { 
            New-ADOrganizationalUnit -Name $PSItem -Path $Tier1SQLPath
        }
    }
    catch {
        throw "error creating ous: $($PSItem.Exception.Message)"
    }
    #endregion

    #region groups
    try {
        Write-Output "$($env:COMPUTERNAME): creating groups"
        # PAW
        $GroupNames = 'T0-Allowed', 'T1-Allowed', 'T2-Allowed', 'T0-Denied', 'T1-Denied', 'T2-Denied'
        $GroupNames | ForEach-Object { 
            New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$ESAEPath"
        }
        $PAWGroupNames = 'PAW-Devices', 'PAW-Maintenance', 'PAW-Users'
        $PAWGroupNames | ForEach-Object { 
            New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$PAWPath"
        }

        # T0
        $Tier0ADGroupNames = 'T0-CloudAdmins', 'T0-DomAdmins', 'T0-EntAdmins', 'T0-RODCAdmins', 'T0-SchAdmins'
        $Tier0ADGroupNames | ForEach-Object { 
            New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier0ADPath"
        }
        $Tier0PKIGroupNames = 'T0-PKIAdmins', 'T0-PKISCAgents'
        $Tier0PKIGroupNames | ForEach-Object {
            New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier0PKIPath"
        }
        $Tier0SQLGroupNames = 'T0-SQLAdmins', 'T0-SQLServers'
        $Tier0SQLGroupNames | ForEach-Object { 
            New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier0SQLPath"
        }
        $Tier0ADFSGroupNames = 'T0-ADFSAdmins', 'T0-ADFSServers', 'T0-ADFSSQLServers'
        $Tier0ADFSGroupNames | ForEach-Object { 
            New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier0ADFSPath"
        }
        $Tier0ADSyncGroupNames = 'T0-ADSyncAdmins', 'T0-ADSyncServers', 'T0-ADSyncSQLServers'
        $Tier0ADSyncGroupNames | ForEach-Object { 
            New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier0ADSyncPath"
        }

        # T1
        $Tier1ExchangeGroupNames = 'T1-ExAdmins', 'T1-ExAllowed', 'T1-ExMaintenance', 'T1-ExMBXAdmins', 'T1-ExOrgAdmins'
        $Tier1ExchangeGroupNames | ForEach-Object { 
            New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier1ExchangePath"
        }
        $Tier1CMGroupNames = 'T1-CMAdmins', 'T1-CMServers', 'T1-CMSQLServers'
        $Tier1CMGroupNames | ForEach-Object { 
            New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier1ConfigMgrPath"
        }
        $Tier1SQLGroupNames = 'T1-SQLAdmins', 'T1-SQLServers'
        $Tier1SQLGroupNames | ForEach-Object { 
            New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier1SQLPath"
        }

        # T2
        $Tier2GroupNames = 'T2-HelpDesk'
        $Tier2GroupNames | ForEach-Object { 
            New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier2Path"
        }
    }
    catch {
        throw "error creating groups: $($PSItem.Exception.Message)"
    }
    #endregion

    #region users
    try {
        Write-Output "$($env:COMPUTERNAME): creating users"
        $Password = ConvertTo-SecureString -String 'C0mplex' -AsPlainText -Force

        # PAW
        $PAWUserNames = 'PAWMan1'
        $PAWUserNames | ForEach-Object { 
            New-ADUser -Name $PSItem -Path "OU=Accounts,$PAWPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true
        }
    
        # T0
        $Tier0ADUserNames = 'T0-DomAdmin1', 'T0-EntAdmin1'
        $Tier0ADUserNames | ForEach-Object { 
            New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier0ADPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true
        }
        $Tier0PKIUserNames = 'T0-PKIAdmin1', 'T0-PKISCA1'
        $Tier0PKIUserNames | ForEach-Object { 
            New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier0PKIPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true
        }
        $Tier0SQLUserNames = 'T0-SQLAdmin1', 'T0-SQLAdmin2'
        $Tier0SQLUserNames | ForEach-Object { 
            New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier0SQLPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true
        }
        $Tier0ADFSUserNames = 'T0-ADFSAdmin1', 'T0-ADFSAdmin2'
        $Tier0ADFSUserNames | ForEach-Object { 
            New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier0ADFSPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true
        }
        $Tier0ADSyncUserNames = 'T0-ADSyncAdmin1', 'T0-ADSyncAdmin2'
        $Tier0ADSyncUserNames | ForEach-Object { 
            New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier0ADSyncPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true
        }
    
        # T1
        $Tier1ExchangeUserNames = 'T1-ExAdmin1', 'T1-ExAdmin2'
        $Tier1ExchangeUserNames | ForEach-Object { 
            New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier1ExchangePath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true
        }
        $Tier1CMUserNames = 'T1-CMAdmin1', 'T1-CMAdmin2'
        $Tier1CMUserNames | ForEach-Object { 
            New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier1ConfigMgrPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true
        }
        $Tier1SQLUserNames = 'T1-SQLAdmin1', 'T1-SQLAdmin2'
        $Tier1SQLUserNames | ForEach-Object { 
            New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier1SQLPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true
        }
    
        # T2
        $Tier2UserNames = 'T2-HelpDesk1', 'T2-HelpDesk2'
        $Tier2UserNames | ForEach-Object { 
            New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier2Path" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true
        }
    }
    catch {
        throw "error creating users: $($PSItem.Exception.Message)"
    }
    #endregion

    #region gmsa
    try {
        Write-Output "$($env:COMPUTERNAME): creating gmsa"
        Add-KdsRootKey -EffectiveTime (Get-Date).AddHours(-10) | Out-Null
        # T0
        New-ADServiceAccount -Name "T0-SQLSvc" -DNSHostName "T0-SQLSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-SQLServers"
        New-ADServiceAccount -Name "T0-SQLAgt" -DNSHostName "T0-SQLAgt.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-SQLServers"
        # T0\ADFS
        New-ADServiceAccount -Name "T0-ADFSSvc" -DNSHostName "T0-ADFSSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-ADFSServers"
        New-ADServiceAccount -Name "T0-ADFSSQLSvc" -DNSHostName "T0-ADFSSQLSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-ADFSSQLServers"
        New-ADServiceAccount -Name "T0-ADFSSQLAgt" -DNSHostName "T0-ADFSSQLAgt.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-ADFSSQLServers"
        # T0\ADSync
        New-ADServiceAccount -Name "T0-ADSyncSvc" -DNSHostName "T0-ADSyncSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-ADSyncServers"
        New-ADServiceAccount -Name "T0-ADSyncSQLSvc" -DNSHostName "T0-ADSyncSQLSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-ADSyncSQLServers"
        New-ADServiceAccount -Name "T0-ADSyncSQLAgt" -DNSHostName "T0-ADSyncSQLAgt.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-ADSyncSQLServers"
    
        # T1
        New-ADServiceAccount -Name "T1-SQLSvc" -DNSHostName "T1-SQLSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T1-SQLServers"
        New-ADServiceAccount -Name "T1-SQLAgt" -DNSHostName "T1-SQLAgt.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T1-SQLServers"
        # T1\ConfigMgr
        New-ADServiceAccount -Name "T1-CMSQLSvc" -DNSHostName "T1-CMSQLSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T1-CMSQLServers"
        New-ADServiceAccount -Name "T1-CMSQLAgt" -DNSHostName "T1-CMSQLAgt.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T1-CMSQLServers"
    }
    catch {
        throw "error creating gmsa: $($PSItem.Exception.Message)"
    }
    #endregion

    #region group membership
    try {
        Write-Output "$($env:COMPUTERNAME): creating group membership"
        # Tier Groups
        $Tier0AllowedGroups = 'T0-CloudAdmins', 'T0-DomAdmins', 'T0-EntAdmins', 'T0-RODCAdmins', 'T0-SchAdmins', 'T0-PKIAdmins', 'T0-PKISCAgents', 'T0-SQLAdmins', 'T0-ADFSAdmins', 'T0-ADSyncAdmins'
        $Tier1AllowedGroups = 'T1-CMAdmins', 'T1-ExAdmins', 'T1-ExAllowed', 'T1-ExMaintenance', 'T1-ExMBXAdmins', 'T1-ExOrgAdmins', 'T1-SQLAdmins'
        $Tier2AllowedGroups = 'T2-HelpDesk'
        Add-ADGroupMember -Identity 'T0-Allowed' -Members $Tier0AllowedGroups
        Add-ADGroupMember -Identity 'T0-Denied' -Members ($Tier1AllowedGroups + $Tier2AllowedGroups)
        Add-ADGroupMember -Identity 'T1-Allowed' -Members $Tier1AllowedGroups
        Add-ADGroupMember -Identity 'T1-Denied' -Members ($Tier0AllowedGroups + $Tier2AllowedGroups)
        Add-ADGroupMember -Identity 'T2-Allowed' -Members $Tier2AllowedGroups
        Add-ADGroupMember -Identity 'T2-Denied' -Members ($Tier0AllowedGroups + $Tier1AllowedGroups)

        # PAW
        Add-ADGroupMember -Identity 'PAW-Maintenance' -Members 'PAWMan1'

        # T0
        Add-ADGroupMember -Identity 'Domain Admins' -Members 'T0-DomAdmin1'
        Add-ADGroupMember -Identity 'Enterprise Admins' -Members 'T0-EntAdmin1'
        Add-ADGroupMember -Identity 'T0-PKIAdmins' -Members 'T0-PKIAdmin1'
        Add-ADGroupMember -Identity 'T0-PKISCAgents' -Members 'T0-PKISCA1'
        Add-ADGroupMember -Identity 'T0-SQLAdmins' -Members 'T0-SQLAdmin1', 'T0-SQLAdmin2'

        # T1
        Add-ADGroupMember -Identity 'T1-ExAdmins' -Members 'T1-ExAdmin1', 'T1-ExAdmin2'
        Add-ADGroupMember -Identity 'T1-CMAdmins' -Members 'T1-CMAdmin1', 'T1-CMAdmin2'
        Add-ADGroupMember -Identity 'T1-SQLAdmins' -Members 'T1-SQLAdmin1', 'T1-SQLAdmin2'

        # T2
        Add-ADGroupMember -Identity 'T2-HelpDesk' -Members 'T2-HelpDesk1', 'T2-HelpDesk2'
    }
    catch {
        throw "error creating group memberships: $($PSItem.Exception.Message)"
    }
    #endregion
}

function Repair-DFSRReplication {
    <#
        .Description
        this will forcefully repair dfsr replication on the domain, use with care
 
        .Example
        # this will forcefully repair dfsr replication on the domain
        Repair-DFSRReplication
 
        .NOTES
        must be executed on a domain controller
         
    #>


    $Domain = (Get-ADDomain -Current LoggedOnUser).DistinguishedName
    $PDC = (Get-ADDomain).PDCEmulator
    $DCs = Get-ADObject -Filter { (objectclass -eq "computer") } -SearchBase "OU=Domain Controllers,$Domain" -Properties Name | Sort-Object Name
    
    $DCs | ForEach-Object {
        Invoke-Command -ComputerName $PSItem.Name -ScriptBlock {
            $Domain = (Get-ADDomain -Current LoggedOnUser).DistinguishedName
            $PDC = (Get-ADDomain).PDCEmulator
            $sysvolADObject = Get-ADObject -Filter { (objectclass -eq "msDFSR-Subscription") } `
                -SearchBase "CN=SYSVOL Subscription,CN=Domain System Volume,CN=DFSR-LocalSettings,CN=$($env:COMPUTERNAME),OU=Domain Controllers,$Domain"`
                -Properties "msDFSR-Options", "msDFSR-Enabled"
    
            Write-Output "$($env:COMPUTERNAME): Restarting NIC to correct Network Profile"
            $NetAdapter = Get-NetAdapter 
            $NetAdapter[0] | Restart-NetAdapter -Confirm:$false

            Write-Output "$($env:COMPUTERNAME): Installing DFSR Management Tools"
            Install-WindowsFeature -Name RSAT-DFS-Mgmt-Con | Out-Null
            if ($($PDC).Contains($env:COMPUTERNAME)) {
                Write-Output "$($env:COMPUTERNAME): Setting msDFSR-Options to 1"
                Set-ADObject -Identity $sysvolADObject.DistinguishedName -Replace @{"msDFSR-Options" = 1 }
            }
            Write-Output "$($env:COMPUTERNAME): Setting msDFSR-Enabled to false"
            Set-ADObject -Identity $sysvolADObject.DistinguishedName -Replace @{"msDFSR-Enabled" = $false }
        }
    }
    
    Write-Output "$($env:COMPUTERNAME): Forcing AD Replication"
    repadmin /syncall /P /e
    Start-Sleep(10)

    Invoke-Command -ComputerName $PDC -ScriptBlock {
        $Domain = (Get-ADDomain -Current LoggedOnUser).DistinguishedName
        $sysvolADObject = Get-ADObject -Filter { (objectclass -eq "msDFSR-Subscription") } `
            -SearchBase "CN=SYSVOL Subscription,CN=Domain System Volume,CN=DFSR-LocalSettings,CN=$($env:COMPUTERNAME),OU=Domain Controllers,$Domain"`
            -Properties "msDFSR-Options", "msDFSR-Enabled"
        Write-Output "$($env:COMPUTERNAME): Restarting DFSR Service"
        Restart-Service DFSR 
        Start-Sleep(20)
        Write-Output "$($env:COMPUTERNAME): Setting msDFSR-Enabled to true"
        Set-ADObject -Identity $sysvolADObject.DistinguishedName -Replace @{"msDFSR-Enabled" = $true }
        Write-Output "$($env:COMPUTERNAME): Forcing AD Replication"
        repadmin /syncall /P /e
        Start-Sleep(10)
        Write-Output "$($env:COMPUTERNAME): Executing DFSRDIAG"
        dfsrdiag pollad
    }
    
    $DCs | ForEach-Object {
        Invoke-Command -ComputerName $PSItem.Name -ScriptBlock {
            $Domain = (Get-ADDomain -Current LoggedOnUser).DistinguishedName
            $PDC = (Get-ADDomain).PDCEmulator
            $sysvolADObject = Get-ADObject -Filter { (objectclass -eq "msDFSR-Subscription") } `
                -SearchBase "CN=SYSVOL Subscription,CN=Domain System Volume,CN=DFSR-LocalSettings,CN=$($env:COMPUTERNAME),OU=Domain Controllers,$Domain"`
                -Properties "msDFSR-Options", "msDFSR-Enabled"
            if (!$($PDC).Contains($($env:COMPUTERNAME))) {
                Write-Output "$($env:COMPUTERNAME): Restarting DFSR Service"
                Restart-Service DFSR 
                Start-Sleep(20)
                Write-Output "$($env:COMPUTERNAME): Setting msDFSR-Enabled to true"
                Set-ADObject -Identity $sysvolADObject.DistinguishedName -Replace @{"msDFSR-Enabled" = $true }
                Write-Output "$($env:COMPUTERNAME): Executing DFSRDIAG"
                dfsrdiag pollad  
            }
        }
    }
}

function Set-Interface {
    <#
        .Description
        configures the network interface, ip, dns, gateway
 
        .Parameter InterfaceObject
        nic objects
 
        .Parameter IPAddress
        ipaddress
 
        .Parameter NetPrefix
        net prefix, e.g. 24
 
        .Parameter DefaultGateway
        default gateway in the subnet
 
        .Parameter DNSAddresses
        dns server addresses
 
        .Parameter NewName
        new name of the network adapter
 
        .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
         
    #>


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

        [Parameter(Mandatory = $true)]
        $IPAddress,

        [Parameter(Mandatory = $true)]
        $NetPrefix,

        [Parameter(Mandatory = $false)]
        $DefaultGateway,

        [Parameter(Mandatory = $false)]
        $DNSAddresses,

        [Parameter(Mandatory = $false)]
        $NewName
    )

    try {
        Write-Host "$($env:COMPUTERNAME): configuring nic with macaddress '$($InterfaceObject.MacAddress)'"
        If (($InterfaceObject | Get-NetIPConfiguration).IPv4Address.IPAddress) {
            $InterfaceObject | Remove-NetIPAddress -AddressFamily "IPv4" -Confirm:$false
        }
        If (($InterfaceObject | Get-NetIPConfiguration).Ipv4DefaultGateway) {
            $InterfaceObject | Remove-NetRoute -AddressFamily "IPv4" -Confirm:$false
        }
        Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\services\Tcpip\Parameters\Interfaces\$($InterfaceObject.InterfaceGuid)" -Name EnableDHCP -Value 0
        Start-Sleep -Seconds 2
        
        if ($null -ne $DefaultGateway) {
            $InterfaceObject | New-NetIPAddress -IPAddress $IPAddress -AddressFamily "IPv4" -PrefixLength $NetPrefix -DefaultGateway $DefaultGateway | Out-Null
            Write-Host "$($env:COMPUTERNAME): nic with mac '$($InterfaceObject.MacAddress)' has static ip '$($IPAddress)'"
        }
        else {
            $InterfaceObject | New-NetIPAddress -IPAddress $IPAddress -AddressFamily "IPv4" -PrefixLength $NetPrefix | Out-Null
            Write-Host "$($env:COMPUTERNAME): nic with mac '$($InterfaceObject.MacAddress)' has static ip '$($IPAddress)'"
        }        
        if ($null -ne $DNSAddresses) {
            $InterfaceObject | Set-DnsClientServerAddress -ServerAddresses $DNSAddresses
        }
        if ($null -ne $NewName) {
            $InterfaceObject | Rename-NetAdapter -NewName $NewName
            Write-Host "$($env:COMPUTERNAME): nic with mac '$($InterfaceObject.MacAddress)' renamed to '$($NewName)'"
        }
    }
    catch {
        throw "error setting '$($InterfaceObject.Name): $($PSItem.Exception.Message)"
    }
}

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"

        Write-Output "$($env:COMPUTERNAME): downloading adk setup files"
        Invoke-WebRequest -UseBasicParsing -Uri $WADK_Download_URL_Latest -OutFile $WADK_LatestPath    
        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) {
            Write-Output "$($env:COMPUTERNAME): downloading adk pe setup files"
            Invoke-WebRequest -UseBasicParsing -Uri $WADK_PE_Download_URL_Latest -OutFile $WADK_PE_LatestPath
    
            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)"
            }
        }    
    }
    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 mp"
        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 dp"
        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 featuresfor 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 sup"
        Install-WindowsFeature -Name RDC, UpdateServices-RSAT -IncludeAllSubFeature | Out-Null
    }
    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 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 = $true)]
        [string]
        $CM_SiteServerFQDN,

        [Parameter(Mandatory = $true)]
        [string]
        $CM_SQL_Site_Instance
    )
    
    $CM_PrereqchkLogFilePath = "$($env:SystemDrive)\ConfigMgrPrereq.log"
    $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"
    }

    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): finished checking prerequisites for the site server role & admin console"
}

function Test-FileLock {
    <#
        .Description
        this function test if a file is in use and returns true if so
 
        .Parameter Path
        file path to the file
 
        .Example
        # this checks if the file is in use
        Test-FileLock -Path C:\WINDOWS\CCM\Logs\PolicyAgentProvider.log
 
        .NOTES
        https://stackoverflow.com/questions/24992681/powershell-check-if-a-file-is-locked
    #>


    [CmdletBinding()]
    param (
        [parameter(Mandatory=$true)]
        [string]
        $Path
    )

    $oFile = New-Object System.IO.FileInfo $Path
    if ((Test-Path -Path $Path) -eq $false) {
        return $false
    }
    try {
        $oStream = $oFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)

        if ($oStream) {
            $oStream.Close()
        }
        $false
    }
    catch {
        # file is locked by a process.
        return $true
    }
}

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
        this function calls SMSSETUP\BIN\X64\Setupdl.exe from configmgr iso
 
        .Parameter Outpath
        save path of the downloaded files
 
        .Example
        # downloads the files to $($env:SystemDrive)\Temp\ConfigMgr\SetupFiles
        Get-CM_SetupFiles -Outpath "$($env:SystemDrive)\Temp\ConfigMgr\SetupFiles"
 
        .NOTES
        https://learn.microsoft.com/en-us/mem/configmgr/core/servers/deploy/install/setup-downloader
    #>


    [CmdletBinding()]
    param (
        [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"
        $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"
        }
    
        if (Test-Path -Path $LogFilePath) {
            Remove-Item -Path $LogFilePath -Force | Out-Null
        }
            
        try {
            Write-Output "$($env:COMPUTERNAME): starting download of configmgr setup 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 D:\SMSSETUP\BIN\X64\Setupdl.exe: FINISHED*"
        if ($null -eq $SuccessMessage[0]) {
            throw "no success message, check log $($CM_PrereqchkLogFilePath)"
        }
        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
 
        .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
    #>


    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
        }

        Write-Output "$($env:COMPUTERNAME): extending schema"
        $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"
        }
        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 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 = $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"
        $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"
        }

        $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 Confirm-HyperV {
    <#
        .Description
        this function throws errors, when hyper-v is not installed
 
        .Example
        # this checks if hyper-v is installed
        Confirm-HyperV
 
        .NOTES
         
    #>


    $OS_Info = Get-CimInstance -ClassName Win32_OperatingSystem
    if ($OS_Info.ProductType -eq 3) {
        if ((Get-WindowsFeature -Name Hyper-V).installed -ne $true) {
            throw "Hyper-v not installed"
        }
    }
    elseif ($OS_Info.ProductType -eq 1) {
        if ((Get-WindowsOptionalFeature -FeatureName Microsoft-Hyper-V -Online).State -ne "Enabled") {
            throw "Hyper-v not installed"
        }
    }    
}