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
 
        .Parameter VDiskRaidLevel
        RAID Level of the virtual disk, allowed is Simple, Mirror, Parity
 
        .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",

        [Parameter(Mandatory = $false)]
        [ValidateSet("Simple", "Mirror", "Parity")]
        [string]
        $VDiskRaidLevel = "Simple",

        [Parameter(Mandatory = $false)]
        [int]
        $VDiskPhysicalDiskRedundancy = 1
    )

    $ErrorActionPreference = 'Stop'

    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 "$($env:COMPUTERNAME): create storage pool $($StoragePoolName)"
            if ($null -eq $SelectedDisks) {
                throw "no disks were found that can be used for the storagepool"
            }
            New-StoragePool -StorageSubSystemFriendlyName $StorageSubSystemFriendlyName -FriendlyName $StoragePoolName -PhysicalDisks $SelectedDisks | Out-Null
            if ($null -eq (Get-VirtualDisk -FriendlyName $VirtualDiskName -ErrorAction SilentlyContinue)) {
                Write-Output "$($env:COMPUTERNAME): create vdisk $($VirtualDiskName)"
                if ($VDiskRaidLevel -ne "Simple") {
                    New-VirtualDisk -StoragePoolFriendlyName $StoragePoolName `
                        -FriendlyName $VirtualDiskName -UseMaximumSize `
                        -ProvisioningType Fixed `
                        -ResiliencySettingName $VDiskRaidLevel `
                        -PhysicalDiskRedundancy $VDiskPhysicalDiskRedundancy | Out-Null
                }
                else {
                    New-VirtualDisk -StoragePoolFriendlyName $StoragePoolName `
                        -FriendlyName $VirtualDiskName -UseMaximumSize `
                        -ProvisioningType Fixed `
                        -ResiliencySettingName $VDiskRaidLevel | Out-Null
                }
                Initialize-Disk -FriendlyName $VirtualDiskName -PartitionStyle GPT | Out-Null
                $VDiskNumber = (Get-Disk -FriendlyName $VirtualDiskName).Number
                Write-Output "$($env:COMPUTERNAME): create volume $($VMVolumeName)"
                New-Volume -DiskNumber $VDiskNumber -FriendlyName $VMVolumeName -FileSystem ReFS -DriveLetter $VMDriveLetter | Out-Null
            }
            else {
                Write-Output "$($env:COMPUTERNAME): virtual disk $($VirtualDiskName) already exists - skipping"
            }
        }
        else {
            Write-Output "$($env:COMPUTERNAME): pool $($StoragePoolName) already exists - skipping"
        }
    }
    catch {
        throw "$($env:COMPUTERNAME): 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 "$($env:COMPUTERNAME): 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 "$($env:COMPUTERNAME): virtual vswitch $($Course_Shortcut) already exists - skipping"
        }
    }
    catch {
        throw "$($env:COMPUTERNAME): 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
    )

    Confirm-VMPresence -VM_Config_Obj $VM_Config_Obj
    try {
        Invoke-Command -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials -ScriptBlock {
            function Start-CMCollectionUpdate {
                [CmdletBinding()]
                param (
                    [Parameter(Mandatory = $true)]
                    [string]
                    $CollectionName,

                    [Parameter(Mandatory = $false)]
                    [int]
                    $NotBeforeMinutues = 2
                )
                
                $Collection = Get-CMCollection -Name $CollectionName
                $RefreshTime = (Get-Date).AddHours(-1) - $Collection.IncrementalEvaluationLastRefreshTime
                if ($RefreshTime.TotalMinutes -gt $NotBeforeMinutues) {
                    Write-Output "$($env:COMPUTERNAME): doing cm collection update on $($Collection.Name)"
                    Invoke-CMCollectionUpdate -CollectionId $Collection.CollectionID
                    Start-Sleep -Seconds 5
                }
            }
            function Confirm-CMCollectionMembership {
                [CmdletBinding()]
                param (
                    [Parameter(Mandatory = $true)]
                    [string]
                    $DeviceName,
                    
                    [Parameter(Mandatory = $true)]
                    [string]
                    $CollectionName
                )
                
                $Device_Exists_In_Collection = $false
                $Counter = 0
                Write-Output "$($DeviceName) - $("{0:d2}" -f $Counter): checking collection memberships in $($CollectionName)"
                while ($Device_Exists_In_Collection -eq $false -and $Counter -lt 40) {
                    $CurrentCollectionMembers = Get-CMCollectionMember -CollectionName $CollectionName -Name $DeviceName
                    if ($null -ne $CurrentCollectionMembers) {
                        $Device_Exists_In_Collection = $true
                    }
                    else {
                        Write-Output "$($DeviceName) - $("{0:d2}" -f $Counter): device not found in collection $($CollectionName)"
                        Start-Sleep -Seconds 10
                        $Counter++
                    }
                    if ($Counter -eq 12 -or $Counter -eq 24 -or $Counter -eq 36) {
                        Start-CMCollectionUpdate -CollectionName $CollectionName
                    }
                }
                if ($Counter -ge 40) {
                    throw "$($DeviceName) - $("{0:d2}" -f $Counter): could not find in the collection $($CollectionName)"
                }
                else {
                    Write-Output "$($DeviceName) - $("{0:d2}" -f $Counter): $($CollectionName) - found, continuing"
                }
            }

            $VM_Config_Obj = $using:VM_Config_Obj
            $PSDriveName = "CM-Temp-Drive"
            $CM_Collection_All_Systems_ID = "SMS00001"

            # 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):\"

            $CM_Collection_All_Systems_Name = (Get-CMCollection -Id $CM_Collection_All_Systems_ID).Name
            $CM_SiteCode = (Get-CMSite).SiteCode
            $CM_Devices_Names = $VM_Config_Obj.Keys | Sort-Object

            # check destination collection existance
            $CM_Devices_Names | ForEach-Object {
                if ($null -eq (Get-CMCollection -Name $VM_Config_Obj.$PSItem.CM_Collection_Name)) {
                    throw "collection $($VM_Config_Obj.$PSItem.CM_Collection_Name) does not existing"
                }
            }
            
            # checking existing devices
            $CM_Devices_Names | ForEach-Object {
                $Temp_CMDevice = Get-CMDevice -Name $PSItem
                if (($null -ne $Temp_CMDevice) -and ($Temp_CMDevice.MACAddress.ToString().Replace(":", "") -ne $VM_Config_Obj.$PSItem.MAC)) {
                    Write-Output "$($PSItem): removing existing computer info in configmgr - macaddress $($VM_Config_Obj.$PSItem.MAC), because the mac address is not correct"
                    Remove-CMDevice -Name $PSItem -Force -Confirm:$false
                }
            }
            Start-Sleep -Seconds 5

            # import device
            $CM_Devices_Names | ForEach-Object {
                $Temp_CMDevice = Get-CMDevice -Name $PSItem
                if ($null -eq $Temp_CMDevice) {
                    Write-Output "$($PSItem): creating computer info in configmgr - macaddress $($VM_Config_Obj.$PSItem.MAC)"
                    Import-CMComputerInformation -CollectionName $CM_Collection_All_Systems_Name -ComputerName $PSItem -MacAddress $VM_Config_Obj.$PSItem.MAC
                }
            }

            # checking all system refreshinterval
            Start-CMCollectionUpdate -CollectionName $CM_Collection_All_Systems_Name
            Start-Sleep -Seconds 10

            # check collection membership all systems
            $CM_Devices_Names | ForEach-Object {
                Confirm-CMCollectionMembership -DeviceName $PSItem -CollectionName $CM_Collection_All_Systems_Name
            }

            # create membership rule
            $CM_Devices_Names | ForEach-Object {
                Add-CMDeviceCollectionDirectMembershipRule -CollectionName $VM_Config_Obj.$PSItem.CM_Collection_Name -ResourceID (Get-CMDevice -Name $PSItem).ResourceID
            }
            Start-Sleep -Seconds 5

            # check collection membership target
            $CM_Devices_Names | ForEach-Object {
                Confirm-CMCollectionMembership -DeviceName $PSItem -CollectionName $VM_Config_Obj.$PSItem.CM_Collection_Name
            }

            # remove collections, so there is only the targeted
            $CM_Devices_Names | ForEach-Object {
                $Temp_CMDevice = Get-CMDevice -Name $PSItem
                $TargetCollection = Get-CMCollection -Name $($VM_Config_Obj.$PSItem.CM_Collection_Name)
                if ($null -ne $Temp_CMDevice) {
                    $Collections = Get-CimInstance -Namespace "root/Sms/site_$($CM_SiteCode)" -ClassName "SMS_FullCollectionMembership" -Filter "ResourceID = $($Temp_CMDevice.ResourceID)" | `
                        Where-Object -Property CollectionID -ne $CM_Collection_All_Systems_ID | `
                        Where-Object -Property CollectionID -ne $TargetCollection.CollectionID
                    if ($null -ne $Collections) {
                        $Collections | ForEach-Object {                            
                            $MembershipRule = Get-CMCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID | Where-Object -Property RuleName -EQ $Temp_CMDevice.Name
                            if ($null -ne $MembershipRule) {
                                Write-Output "$($Temp_CMDevice.Name): removing collection membership in $((Get-CMCollection -Id $PSItem.CollectionID).Name)"
                                Remove-CMDeviceCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID -ResourceId $MembershipRule.ResourceId -Confirm:$false -Force
                            }
                        }
                    }
                }
            }

            Set-Location -Path $env:SystemDrive
            Remove-PSDrive -Name $PSDriveName
        }
        $SecondsToWait = 120
        Write-Output "$($env:COMPUTERNAME): finished registration, 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 {
        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 {
        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
                if ($null -ne $CM_All_Deployments) {
                    Write-Output "timestamp : $(Get-Date -format "yyyy-MM-dd_HH.mm.ss") - $($Deployment_Check_Count)"
                    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, check the logs in configmgr or inside the vms"
            }

            Set-Location -Path $env:SystemDrive
            Remove-PSDrive -Name $PSDriveName
        }
    }
    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"
        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 Version
        version of the iso
 
        .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)]
        [ValidateSet("2019", "2022")]
        [string]
        $Version,

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

    switch ($Version) {
        "2019" { $DownloadURL = "https://download.microsoft.com/download/4/8/6/486005eb-7aa8-4128-aac0-6569782b37b0/SQL2019-SSEI-Eval.exe" }
        "2022" { $DownloadURL = "https://go.microsoft.com/fwlink/?linkid=2215202&clcid=0x409&culture=en-us&country=us" }
        Default { throw "no version was selected" }
    }

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

    # downloading eval setup
    try {
        Start-FileDownload -DownloadURL $DownloadURL -FileOutPath $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

        Remove-Item -Path $SQLEvalSetupFullPath -Force
    }
    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 = $false)]
        [ValidateSet("2019", "2022")]
        [string]
        $Version = "2019",

        [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 -Version $Version -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): copy files from iso to $($SQLFileFolder)"
        Copy-Item -Path "$($Volume.DriveLetter):\" -Destination $SQLFileFolder -Recurse -Force
        Write-Output "$($env:COMPUTERNAME): finished copy job"
        Dismount-DiskImage $MountedISOs.ImagePath | Out-Null
        Write-Output "$($env:COMPUTERNAME): iso was dismounted"
    }
    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 -Wait
        Start-Sleep -Seconds 5
        Start-Process -FilePath klist -ArgumentList "purge -lh 0 -li 0x3e7" -NoNewWindow -Wait
        Start-Sleep -Seconds 10
        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 ($SQLSYSADMINACCOUNTS -eq "") {
        $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 instance $($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="' + $env:ProgramFiles + '\Microsoft SQL Server"')
            ('/INSTALLSHAREDWOWDIR="' + ${env:ProgramFiles(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) {
            $Message = Search-SQLSummaryLog -SearchString "Exit message"
            throw "check logs of sql setup - $($env:ProgramFiles)\Microsoft SQL Server\*\Setup Bootstrap\Log `nmessage found: $($Message)"
        }
    }
    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 instance $($Name)"
}

function Set-SQLInstanceStaticPort {
    <#
        .Description
        configures the sql instance to use a static tcp port for all ips
 
        .Parameter InstanceName
        sql instance name
         
        .Parameter StaticPort
        port which should be used
 
        .Example
        # this installs an sql instance with the name PB1 which uses gmsa with customized file paths
         
        .Example
        # this installs an sql instance with the name INSTANCE and local accounts to default file paths
        Set-SQLInstanceStaticPort -InstanceName "INSTANCE" -StaticPort "1503"
 
        .NOTES
        Support is only available for SQL 2019 Standard / Enterprise
    #>

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

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

    $RegPath = (Resolve-Path -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL*$($InstanceName)\MSSQLServer\SuperSocketNetLib\Tcp\IPAll\").Path
    
    if (Test-Path -Path $RegPath) {
        try {
            $SQLSettings = Get-ItemProperty -Path $RegPath
            if ($SQLSettings.TcpDynamicPorts -ne $StaticPort) {
                Write-Output "$($env:COMPUTERNAME): set tcp port of $($InstanceName) to $($StaticPort)"
                Set-ItemProperty -Path $RegPath -Name TcpPort -Value $StaticPort
                Set-ItemProperty -Path $RegPath -Name TcpDynamicPorts -Value ""
            }
            Write-Output "$($env:COMPUTERNAME): restarting $($InstanceName) to apply change"
            Get-Service "*$($InstanceName)*" | Restart-Service -Force -WarningAction SilentlyContinue
        }
        catch {
            throw "could not configure the static port on $($InstanceName) - $($PSItem.Exception.Message)"
        }
    }
    else {
        throw "could not find the registry path for the instance"
    }
}

function Test-SQLDatabaseConnection {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True)] 
        [string] 
        $Server,

        [Parameter(Position = 1, Mandatory = $false)] 
        [string] 
        $Database = "master",

        [Parameter(Position = 2, Mandatory = $True, ParameterSetName = "SQLAuth")] 
        [pscredential] 
        $SACredential,

        [Parameter(Position = 2, Mandatory = $True, ParameterSetName = "WindowsAuth")] 
        [switch] 
        $UseWindowsAuthentication
    )

    if ($Server -notlike "*\*") {
        $Server = "$($env:COMPUTERNAME)\$($Server)"
    }

    # connect to the database, then immediatly close the connection. If an exception occurrs it indicates the conneciton was not successful.
    $dbConnection = New-Object System.Data.SqlClient.SqlConnection
    if (!$UseWindowsAuthentication) {
        $dbConnection.ConnectionString = "Data Source=$($Server); uid=$($SACredential.UserName); pwd=$($SACredential.GetNetworkCredential().Password); Database=$($Database);Integrated Security=False"
    }
    else {
        $dbConnection.ConnectionString = "Data Source=$($Server); Database=$($Database);Integrated Security=True;"
    }
    try {
        Measure-Command { $dbConnection.Open() } | Out-Null
        $Success = $true
    }
    # exceptions will be raised if the database connection failed.
    catch {
        $Success = $false
    }
    Finally {
        # close the database connection
        $dbConnection.Close()
    }

    return $Success
}

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


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("2019_latest")]
        [string]
        $Version,

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

    $ErrorActionPreference = 'Stop'
    if ($Outpath[-1] -eq "\") {
        $Outpath = $Outpath.Substring(0, $Outpath.Length - 1)
    }
    $SQLCUFilePath = "$($Outpath)\SQL-$($Version)-cu.exe"

    switch ($Version) {
        "2019_latest" { $DownloadURL = "https://www.microsoft.com/en-us/download/confirmation.aspx?id=100809" }
        Default { throw "no version was selected or not supported" }
    }

    try {
        $Content = Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL
        $UpdateLink = ($Content.Links | Where-Object -FilterScript { $PSItem.href -like "*download.microsoft.com*" -and $PSItem.outerHTML -like "*download manually*" }).href

        Start-FileDownload -DownloadURL $UpdateLink -FileOutPath $SQLCUFilePath
    }
    catch {
        throw "$($env:COMPUTERNAME): error getting sql cu files - $($PSItem.Exception.Message)"
    }

    Write-Output "$($env:COMPUTERNAME): finished download - check folder $($Outpath)"
}

function Install-SQLCU {
    <#
        .Description
        installs sql 2019 latest cu to all local instances
 
        .Parameter Version
        version string of sql cu, like 2019_latest
 
        .Parameter TempFolder
        folder, where files will be stored temporally
 
        .Example
        # this installes sql 2019 latest cu to all instances
        Install-SQLCU -Version "2019_latest"
 
        .NOTES
 
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("2019_latest")]
        [string]
        $Version,

        [Parameter(Mandatory = $false)]
        [string]
        $TempFolder = "$($env:ProgramData)\NTS\SQL\CU"
    )

    $ErrorActionPreference = 'Stop'
    $SQLCUFilePath = "$($TempFolder)\SQL-$($Version)-cu.exe"
    $SQLCUUnpackedFilePath = "$($TempFolder)\$($Version)-cu\"

    try {
        Get-SQLCU -Version $Version -Outpath $TempFolder

        # unpacking sql cu to file system
        if ((Test-Path -Path $SQLCUFilePath) -eq $false) {
            throw "cannot find cu package file"
        }
        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"
        }
    }
    catch {
        throw "$($env:COMPUTERNAME): error getting sql cu files - $($PSItem.Exception.Message)"
    }
    
    try {
        # installation
        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) {
            $FinalResult = Search-SQLSummaryLog -SearchString "Final result"
            $ExitMessage = Search-SQLSummaryLog -SearchString "Exit message"

            if ($ExitMessage -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"
            }
            elseif ($FinalResult -like "*Passed but reboot required, see logs for details*") {
                Write-Output "$($env:COMPUTERNAME): sql cu install but reboot required"
            }
            else {
                throw "there was an error installing sql cu - $($ExitMessage)"
            }
        }
    
        # cleanup
        Start-FolderCleanUp -FolderToRemove $TempFolder
    }
    catch {
        throw "$($env:COMPUTERNAME): error sql cu installation - $($PSItem.Exception.Message) - see logs at C:\Program Files\Microsoft SQL Server\*\Setup Bootstrap\Log\"
    }
}

function Install-SQLSSMS {
    <#
        .Description
        this installs the latest version of sql sql management studio
 
        .Example
        # this installs the latest version of sql ssms
        Install-SQLSSMS
 
        .NOTES
        always installs the latest version from https://aka.ms/ssmsfullsetup
    #>


    $LatestSetupURL = "https://aka.ms/ssmsfullsetup"
    $TempFolderPath = "$($env:ProgramData)\NTS\SQL\SSMS\"
    $SetupFilePath = $TempFolderPath + "SSMS-Latest.exe"

    # download
    try {
        if ((Test-Path -Path $TempFolderPath) -eq $false) {
            New-Item -Path $TempFolderPath -ItemType Directory -Force | Out-Null
        }
        Write-Output "$($env:COMPUTERNAME): downloading SSMS"
        Start-FileDownload -DownloadURL $LatestSetupURL -FileOutPath $SetupFilePath
    }
    catch {
        throw "error downloading the setup - $($PSItem.Exception.Message)"
    }

    # install
    try {
        Write-Output "$($env:COMPUTERNAME): installing SSMS"
        Start-Process -FilePath $SetupFilePath -ArgumentList "/quiet /norestart" -Wait -NoNewWindow
        Write-Output "$($env:COMPUTERNAME): finished installing SSMS"
    
        Write-Output "$($env:COMPUTERNAME): doing cleanup"
        Remove-Item -Path $TempFolderPath -Recurse -Force
    }
    catch {
        throw "error installing - $($PSItem.Exception.Message)"
    }
}

function Get-SQLSSRSSetup {
    <#
        .Description
        this downloads the sql reporting services setup
 
        .Parameter Version
        version of sql reporting services setup
 
        .Parameter Outpath
        where should the setup file be stored
 
        .Example
        # this will download the setup to $SSRSTempFolder
        Get-SQLSSRSSetup -TempFolder $SSRSTempFolder
 
        .NOTES
        setup file name will be "$($TempFolder)\SQL$($Version)_ReportingServices-latest.exe"
    #>


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

        [Parameter(Mandatory = $false)]
        [string]
        $Outpath = "$($env:ProgramData)\NTS\SQL\ReportingServices"
    )
    
    try {
        $SQL_RPServices_FilePath = "$($Outpath)\SQL$($Version)_ReportingServices-latest.exe"
        if ((Test-Path -Path $Outpath) -eq $false) {
            New-Item -Path $Outpath -ItemType Directory -Force | Out-Null
        }
        if ($Version -eq "2019") {
            $DownloadURL = "https://www.microsoft.com/en-us/download/confirmation.aspx?id=100122"
        }
        $Content = Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL
        $SetupLink = ($Content.Links | Where-Object -FilterScript { $PSItem.href -like "*download.microsoft.com*" -and $PSItem.outerHTML -like "*download manually*" }).href
        Write-Output "$($env:COMPUTERNAME): downloading sql reporting services"
        Start-FileDownload -DownloadURL $SetupLink -FileOutPath $SQL_RPServices_FilePath
    }
    catch {
        throw "$($env:COMPUTERNAME): - $($PSItem.Exception.Message)"
    }
}

function Install-SQLSSRS {
    <#
        .Description
        this will install sql reporting services
 
        .Parameter Version
        version of sql reporting services setup
 
        .Parameter SetupFilePath
        path to setup file
 
        .Example
        # this will install ssrs to the local server
        Install-SQLSSRS -Version 2019
 
        .NOTES
        setup file name must be at "$($TempFolder)\SQL$($Version)_ReportingServices-latest.exe"
    #>


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

        [Parameter(Mandatory = $false)]
        [string]
        $SetupFilePath = "$($env:ProgramData)\NTS\SQL\ReportingServices\SQL$($Version)_ReportingServices-latest.exe",

        [Parameter(Mandatory = $false)]
        [string]
        $SQL_RPServices_LogPath = "$($env:ProgramData)\NTS\SQL\ReportingServices\install.log"
    )

    try {
        if (Test-Path -Path $SetupFilePath) {
            Write-Output "$($env:COMPUTERNAME): installing sql reporting services"
            $Arguments = "/quiet /IAcceptLicenseTerms /Edition=Eval /norestart /log $($SQL_RPServices_LogPath)"
            $Process = Start-Process $SetupFilePath -ArgumentList $Arguments -Wait -NoNewWindow -PassThru
            if ($Process.ExitCode -ne 0) {
                throw "error installing ssrs - check logs at $($SQL_RPServices_LogPath)"
            }
        }
        else {
            throw "setup file not found at $($SetupFilePath)"
        }
    }
    catch {
        throw "$($env:COMPUTERNAME): - $($PSItem.Exception.Message)"
    }
}

function Initialize-SQLSSRS {
    <#
        .Description
        this will configure the sql reporting services
 
        .Parameter Version
        version of sql reporting services setup
 
        .Parameter SQL_Instance
        mssql instance
 
        .Parameter SSRS_ServiceAccountCredentials
        credentials for the service account of ssrs, must be an domain user
 
        .Parameter SQL_DB_Name
        name of ssrs db
 
        .Example
        # this will configure the ssrs to use a domain account as service
        Initialize-SQLSSRS -SQL_Instance "$($env:COMPUTERNAME)\$($using:CM_SQL_RPT_InstanceName)" -SSRS_ServiceAccountCredentials $using:CM_SQL_SSRS_ServiceAccountCredentials -TempFolder $SSRSTempFolder
 
        .NOTES
        https://blog.aelterman.com/2018/01/03/complete-automated-configuration-of-sql-server-2017-reporting-services/
        https://github.com/mrsquish/AutomationScripts/blob/main/ConfigureSSRS.ps1
        https://gist.github.com/SvenAelterman/f2fd058bf3a8aa6f37ac69e5d5dd2511
    #>


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

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

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

        [Parameter(Mandatory = $false)]
        [string]
        $SQL_DB_Name = "ReportServer"
    )

    switch ($Version) {
        "2019" { $WMI_Namespace = "root\Microsoft\SqlServer\ReportServer\RS_SSRS\v15\Admin" }
        "2017" { $WMI_Namespace = "root\Microsoft\SqlServer\ReportServer\RS_SSRS\v14\Admin" }
        Default { throw "no version was selected or the version is unsupported" }
    }

    if ($SQL_Instance -notlike "*\*") {
        $SQL_Instance = "$($env:COMPUTERNAME)\$($SQL_Instance)"
    }

    # test sql instance connection
    if ((Test-SQLDatabaseConnection -Server $SQL_Instance -UseWindowsAuthentication) -eq $false) {
        throw "could not connect to sql instance $($SQL_Instance) using integrated security"
    }

    try {
        $RSConfig = Get-WmiObject -Class "MSReportServer_ConfigurationSetting" -Namespace $WMI_Namespace
        If ($RSConfig.IsInitialized -eq $true) {
            throw "ssrs is already initialized, stopping"
        }
        Write-Output "$($env:COMPUTERNAME): configuring sql reporting services"
    
        # set service account
        $RSConfig = Get-WmiObject -Class "MSReportServer_ConfigurationSetting" -Namespace $WMI_Namespace
        $useBuiltInServiceAccount = $false
        Write-Output "$($env:COMPUTERNAME): configuring $($SSRS_ServiceAccountCredentials.UserName) as service account for ssrs"
        $RSConfig.SetWindowsServiceIdentity($useBuiltInServiceAccount, $($SSRS_ServiceAccountCredentials.UserName), $($SSRS_ServiceAccountCredentials.GetNetworkCredential().Password)) | out-null
    
        # need to reset the URLs for domain service account to work
        Write-Output "$($env:COMPUTERNAME): configuring http urls"
        $HTTPport = 80
        $RSConfig.RemoveURL("ReportServerWebService", "http://+:$($HTTPport)", 1033) | out-null
        $RSConfig.RemoveURL("ReportServerWebApp", "http://+:$($HTTPport)", 1033) | out-null
        $RSConfig.SetVirtualDirectory("ReportServerWebService", "ReportServer", 1033) | out-null
        $RSConfig.SetVirtualDirectory("ReportServerWebApp", "Reports", 1033) | out-null
        $RSConfig.ReserveURL("ReportServerWebService", "http://+:$($HTTPport)", 1033) | out-null
        $RSConfig.ReserveURL("ReportServerWebApp", "http://+:$($HTTPport)", 1033) | out-null
    
        # restart SSRS service for changes to take effect
        # Restart-Service -Name $RSConfig.ServiceName -Force
    
        # retrieve the current configuration
        $RSConfig = Get-WmiObject -Class "MSReportServer_ConfigurationSetting" -Namespace $WMI_Namespace
        # get the ReportServer and ReportServerTempDB creation script
        [string]$dbscript = $RSConfig.GenerateDatabaseCreationScript($SQL_DB_Name, 1033, $false).Script
    
        # import the SQL Server PowerShell module
        Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force -Scope Process
        Import-Module sqlps -DisableNameChecking | Out-Null

        # establish a connection to the database server
        # $ErrorActionPreference = 'SilentlyContinue'
        # $RunCount = 0
        # do {
        Write-Output "$($env:COMPUTERNAME): connecting to instance $($SQL_Instance)"
        $conn = New-Object Microsoft.SqlServer.Management.Common.ServerConnection -ArgumentList $SQL_Instance
        $conn.ApplicationName = "SSRS Configuration Script"
        $conn.StatementTimeout = 0
        $conn.Connect()
        # Start-Sleep -Seconds 5
        # $RunCount++
        # }
        # while ($conn.IsOpen -eq $false -and $RunCount -lt 6)
        # $ErrorActionPreference = 'Continue'

        if ($conn.IsOpen -ne $true) {
            throw "could not connect to $($SQL_Instance)"
        }
        $smo = New-Object Microsoft.SqlServer.Management.Smo.Server -ArgumentList $conn

        # create the ReportServer and ReportServerTempDB databases
        Write-Output "$($env:COMPUTERNAME): generating databases in instance $($SQL_Instance)"
        $db = $smo.Databases["master"]
        $db.ExecuteNonQuery($dbscript)
    
        # set permissions for the databases
        $dbscript = $RSConfig.GenerateDatabaseRightsScript($RSConfig.WindowsServiceIdentityConfigured, $SQL_DB_Name, $false, $true).Script
        $db.ExecuteNonQuery($dbscript)
    
        # set the database connection info # check hresult auf wert 0
        Write-Output "$($env:COMPUTERNAME): configuring db connection for ssrs to instance $($SQL_Instance) with db $($SQL_DB_Name)"
        $RSConfig.SetDatabaseConnection($SQL_Instance, $SQL_DB_Name, 2, "", "") | Out-Null
        $RSConfig.InitializeReportServer($RSConfig.InstallationID) | Out-Null
    
        # restart services # check hresult auf wert 0
        $RSConfig.SetServiceState($false, $false, $false) | Out-Null
        Restart-Service $RSConfig.ServiceName
        $RSConfig.SetServiceState($true, $true, $true) | Out-Null
    }
    catch {
        throw "$($env:COMPUTERNAME): $($PSItem.Exception.Message)"
    }
}

function Search-SQLSummaryLog {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $SearchString
    )
    
    try {
        $SummaryLogFilePath = (Get-Item -Path "$($env:ProgramFiles)\Microsoft SQL Server\*\Setup Bootstrap\Log\Summary.txt" | Sort-Object -Property LastWriteTimeUtc -Descending | Select-Object -First 1).FullName
        $Content = Get-Content -Path $SummaryLogFilePath

        if ($null -eq $Content) {
            throw "file could not be found"
        }
        else {
            $FilteredContent = $Content | Select-String -Pattern $SearchString
            if ($null -eq $FilteredContent) {
                return "no message found with selected search pattern"
            }
            else {
                $Message = $FilteredContent[0].Tostring().Replace($SearchString, "").Trim()
                if ($Message -like "*:*") {
                    $Message = $Message.Replace(":", "").Trim()
                }
                return $Message
            }
        }
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Start-FolderCleanUp {
    <#
        .Description
        this function can be used to remove folders and its items
 
        .Parameter FolderToRemove
        version of sql reporting services setup
 
        .Example
        # this will remove the folder $SSRSTempFolder and its items
        Start-CleanUp -FolderToRemove $SSRSTempFolder
 
        .NOTES
 
    #>


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

    Write-Output "$($env:COMPUTERNAME): removing temp files from $($FolderToRemove)"
    try {
        Remove-Item -Path $FolderToRemove -Recurse -Force
    }
    catch {
        throw "error while cleanup - $($PSItem.Exception.Message)"
    }
    Write-Output "$($env:COMPUTERNAME): cleanup finished"
}

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)]
        [Int64]
        $VHDXSize = 80GB
    )

    try {
        Write-Output "$($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-Output "$($VMName): configuring network"
        Invoke-Command -VMName $VMName -Credential $VMCredential -ScriptBlock {
            $InterfaceObject = (Get-NetAdapter)[0]
            Write-Output "$($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-Output "$($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 "$($env:COMPUTERNAME): joining vm $($VMName) to 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 "$($env:COMPUTERNAME): domainjoin of vm $($VMName) successfull - reboot required"
        }
        else {
            Write-Output "$($env:COMPUTERNAME): domainjoin of vm $($VMName) successfull - vm will do reboot"
        }        
    }
    catch {
        throw "$($env:COMPUTERNAME): error joining vm $($VMName) 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") {
            Write-Output "$($env:COMPUTERNAME): starting $($VMObject.Name) because the vm was stopped"
            Start-VM -VM $VMObject
            Start-Sleep -Seconds 10
        }
        Write-Output "$($env:COMPUTERNAME): verify the connection to $($VMObject.Name)"
        if (Test-VMConnection -VMId $VMObject.Id -LocalAdminCreds $VMCredential) {
            Write-Output "$($env:COMPUTERNAME): connected to $($VMObject.Name) successful - continue"
        }
        else {
            throw "$($env:COMPUTERNAME): error while connecting to $($VMObject.Name) 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', 'T1-CMSQLRPT'
        $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-Output "$($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-Output "$($env:COMPUTERNAME): interface $($InterfaceObject.InterfaceDescription) has the static ip $($IPAddress) now"
        }
        else {
            $InterfaceObject | New-NetIPAddress -IPAddress $IPAddress -AddressFamily "IPv4" -PrefixLength $NetPrefix | Out-Null
            Write-Output "$($env:COMPUTERNAME): interface $($InterfaceObject.InterfaceDescription) has the static ip $($IPAddress) now"
        }        
        if ($null -ne $DNSAddresses) {
            $InterfaceObject | Set-DnsClientServerAddress -ServerAddresses $DNSAddresses
        }
        if ($null -ne $NewName) {
            $InterfaceObject | Rename-NetAdapter -NewName $NewName
            Write-Output "$($env:COMPUTERNAME): interface $($InterfaceObject.InterfaceDescription) 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"

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

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

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

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


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

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

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


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

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

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


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

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

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


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

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


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

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


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

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

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

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

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

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

function Add-SQLDBRole {
    <#
        .Description
        this function can be used to add a role to db for an sql login
 
        .Parameter InstanceName
        name of the instance
 
        .Parameter SQLogin
        sql login name
 
        .Parameter DBName
        database name
 
        .Parameter DBRole
        role that should be granted to the sql login
 
        .Example
        # adds db_owner to local system on susdb
        Add-SQLDBRole -InstanceName "$($env:COMPUTERNAME)\$using:CM_SQL_WSUS_InstanceName" -SQLogin "NT AUTHORITY\SYSTEM" -DBName "SUSDB" -DBRole 'db_owner'
 
        .NOTES
        https://www.sqlservercentral.com/blogs/use-powershell-to-add-a-login-to-a-database-role-in-all-databases
    #>

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

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

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

        [Parameter(Mandatory = $true)]
        [string]
        $DBRole
    )
    
    try {
        # Load the SMO assembly
        [System.Reflection.Assembly]::LoadWithPartialName( 'Microsoft.SqlServer.SMO') | Out-Null

        # Connect to the instance using SMO
        $SQLServer = new-object ('Microsoft.SqlServer.Management.Smo.Server') $InstanceName

        # Get the defined login - if it doesn't exist it's an error
        $SQLoginObject = $SQLServer.Logins[$SQLogin]
        if ($null -eq $SQLoginObject) {
            throw "$($SQLogin) is not a valid SQL Server Login on this instance."
        }

        $SQLLoginName = $SQLoginObject.Name
        $SQLDatabaseObject = $SQLServer.Databases[$DBName]

        # Check to see if the login is a user in this database
        $SQLUserObject = $SQLDatabaseObject.Users[$SQLLoginName]
        if ($null -eq $SQLUserObject) {
            # Not present, so add it
            $SQLUserObject = New-Object ('Microsoft.SqlServer.Management.Smo.User') ($SQLDatabaseObject, $SQLLoginName)
            $SQLUserObject.Login = $SQLLoginName
            $SQLUserObject.Create()
        }

        # Check to see if the user is a member of the db_owner role
        if ($SQLUserObject.IsMember($DBRole) -ne $True) {
            # Not a member, so add that role
            $SQLConnection = new-object system.data.SqlClient.SqlConnection("Data Source=$($InstanceName);Integrated Security=SSPI;Initial Catalog=$($DBName)");
            $SQLConnection.Open()
            $SQLQuery = "EXEC sp_addrolemember @rolename = N'$($DBRole)', @membername = N'$($SQLLoginName)'"
            $SQLCommand = new-object "System.Data.SqlClient.SqlCommand" ($SQLQuery, $SQLConnection)
            Write-Output "$($env:COMPUTERNAME): adding db role $($DBRole) to $($SQLogin) on instance $($InstanceName) for db $($DBName)"
            $SQLCommand.ExecuteNonQuery() | out-null
            $SQLConnection.Close()
        }    
    }
    catch {
        throw "could not grant sql db role - $($PSItem.Exception.Message)"
    }
}

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


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

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

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

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

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

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

function 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
        downloads the eval setup of configmgr current branch
 
        .Parameter Version
        version of the iso
 
        .Parameter Outpath
        path where the setup file is stored
 
        .Example
        # stores the setup file to $Outpath
        Get-CM_Setupfiles -Version 2203 -Outpath $Outpath
 
        .NOTES
        downloads the configmgr current branch eval setup
    #>


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

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

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

function Get-CM_PrerequisiteFiles {
    <#
        .Description
        this function calls SMSSETUP\BIN\X64\Setupdl.exe from configmgr iso
 
        .Parameter SetupdlFilePath
        path to the Setupdl.exe
 
        .Parameter Outpath
        save path of the downloaded files
 
        .Example
        # downloads the files to $($env:SystemDrive)\Temp\ConfigMgr\SetupFiles
        Get-CM_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 = $false)]
        [string]
        $SetupdlFilePath,

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

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

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


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

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

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

function 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"
        }
    }    
}

function Add-SQLMountPoint {
    <#
        .Description
        this function creates a mountpoint
 
        .Parameter DiskNumber
        number of disk which should be configured
 
        .Parameter SQLBasePath
        where should be mountpoint be created
 
        .Parameter DiskLabel
        label for the volume and the underlaying folders
 
        .Example
        # this checks if hyper-v is installed
        Confirm-HyperV
 
        .NOTES
         
    #>


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

        [Parameter(Mandatory = $false)]
        [string]
        $SQLBasePath = "C:\SQL\",

        [Parameter(Mandatory = $true)]
        [string]
        $DiskLabel
    )
    $MountPath = $SQLBasePath + $DiskLabel
    Write-Output "$($env:COMPUTERNAME): formating disk for $($DiskLabel)"
    Set-Disk -Number $DiskNumber -IsOffline $false
    Set-Disk -Number $DiskNumber -IsReadOnly $false
    Get-Disk -Number $DiskNumber | Initialize-Disk -PartitionStyle GPT -PassThru | New-Partition -UseMaximumSize | Format-Volume -NewFileSystemLabel $DiskLabel -FileSystem ReFS -AllocationUnitSize 65536 -SetIntegrityStreams $false | Out-Null
    New-Item -ItemType Directory -Path $MountPath | Out-Null
    Get-Partition -DiskNumber $DiskNumber | Add-PartitionAccessPath -AccessPath $MountPath -ErrorAction SilentlyContinue

    if ($DiskLabel -like "*LOG*") {
        New-Item -ItemType Directory -Path "$($SQLBasePath)\$($DiskLabel)\LOG" -ErrorAction SilentlyContinue | Out-Null
    }
    else {
        New-Item -ItemType Directory -Path "$($SQLBasePath)\$($DiskLabel)\DATA" -ErrorAction SilentlyContinue | Out-Null
    }
    Start-Sleep -Seconds 2
}

function Set-VMInstallSnapshot {
    <#
        .Description
        this creates a snapshot
 
        .Parameter VMName
        name of vm
 
        .Parameter SnapshotName
        name of snapshot
 
        .Parameter VMCredential
        credentials for vm
 
        .Example
        # this creates a snapshot for the vm
        Set-VMInstallSnapshot -VMName $PSItem -SnapshotName "$(Get-Date -format "yyyy-MM-dd_HH.mm.ss") - initial configuration" -VMCredential $VM_Credentials
 
        .NOTES
        vm will stop and start during this process
    #>


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

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

        [Parameter(Mandatory = $true)]
        [pscredential]
        $VMCredential
    )
    
    Confirm-VMState -VMObject (Get-VM -Name $VMName) -VMCredential $VMCredential
    Stop-VM -Name $VMName -Force
    Write-Output "$($env:COMPUTERNAME): creating snapshot $($VMName) - $($SnapshotName)"
    Checkpoint-VM -Name $VMName -SnapshotName $SnapshotName
    Start-VM -Name $VMName
    Confirm-VMState -VMObject (Get-VM -Name $VMName) -VMCredential $VMCredential
}

function Test-RebootPending {
    ###############################################################################
    # Check-PendingReboot.ps1
    # Andres Bohren / www.icewolf.ch / blog.icewolf.ch / a.bohren@icewolf.ch
    # Version 1.0 / 03.06.2020 - Initial Version - Andres Bohren
    # Version 1.1 / 27.04.2022 - Updated Script - Andres Bohren
    ###############################################################################
    <#
    .SYNOPSIS
        This Script checks diffrent Registry Keys and Values do determine if a Reboot is pending.
     
    .DESCRIPTION
    I found this Table on the Internet and decided to Write a Powershell Script to check if a Reboot is pending.
    Not all Keys are checked. But feel free to extend the Script.
     
    https://adamtheautomator.com/pending-reboot-registry-windows/
    KEY VALUE CONDITION
    HKLM:\SOFTWARE\Microsoft\Updates UpdateExeVolatile Value is anything other than 0
    HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager PendingFileRenameOperations value exists
    HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager PendingFileRenameOperations2 value exists
    HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired NA key exists
    HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Services\Pending NA Any GUID subkeys exist
    HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting NA key exists
    HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce DVDRebootSignal value exists
    HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending NA key exists
    HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootInProgress NA key exists
    HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\PackagesPending NA key exists
    HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttempts NA key exists
    HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon JoinDomain value exists
    HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon AvoidSpnSet value exists
    HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName ComputerName Value ComputerName in HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName is different
     
    .EXAMPLE
    ./Check-PendingReboot.ps1
 
    #>


    function Test-RegistryValue {
        param (
            [parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Path,
            [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()]$Value
        )
        try {
            Get-ItemProperty -Path $Path -Name $Value -EA Stop
            return $true
        }
        catch {
            return $false
        }
    }

    [bool]$PendingReboot = $false

    #Check for Keys
    If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") -eq $true) {
        # Write-Host "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired"
        $PendingReboot = $true
    }

    If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting") -eq $true) {
        # Write-Host "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting"
        $PendingReboot = $true
    }

    If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") -eq $true) {
        # Write-Host "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired"
        $PendingReboot = $true
    }

    If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending") -eq $true) {
        # Write-Host "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending"
        $PendingReboot = $true
    }

    If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttempts") -eq $true) {
        # Write-Host "HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttempts"
        $PendingReboot = $true
    }

    #Check for Values
    If ((Test-RegistryValue -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing" -Value "RebootInProgress") -eq $true) {
        # Write-Host "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing > RebootInProgress"
        $PendingReboot = $true
    }

    If ((Test-RegistryValue -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing" -Value "PackagesPending") -eq $true) {
        # Write-Host "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing > PackagesPending"
        $PendingReboot = $true
    }

    If ((Test-RegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Value "PendingFileRenameOperations") -eq $true) {
        # Write-Host "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager > PendingFileRenameOperations"
        $PendingReboot = $true
    }

    If ((Test-RegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Value "PendingFileRenameOperations2") -eq $true) {
        # Write-Host "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager > PendingFileRenameOperations2"
        $PendingReboot = $true
    }

    If ((Test-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" -Value "DVDRebootSignal") -eq $true) {
        # Write-Host "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce > DVDRebootSignal"
        $PendingReboot = $true
    }

    If ((Test-RegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon" -Value "JoinDomain") -eq $true) {
        # Write-Host "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon > JoinDomain"
        $PendingReboot = $true
    }

    If ((Test-RegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon" -Value "AvoidSpnSet") -eq $true) {
        # Write-Host "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon > AvoidSpnSet"
        $PendingReboot = $true
    }

    return $PendingReboot    
}

function Restart-VMIfNeeded {
    <#
        .Description
        this function checks if the vm has to reboot and does it, if needed
 
        .Parameter VMName
        name of vm
 
        .Parameter Credentials
        credentials for vm
 
        .Example
        # this does a restart of the vm if needed
        Restart-VMIfNeeded -VMName $PSItem -Credential $Domain_Credentials
 
        .NOTES
        vm will stop and start during this process
    #>


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

        [Parameter(Mandatory = $true)]
        [pscredential]
        $Credentials
    )
    $VM = Get-VM -Name $VMName

    $RebootPending = Invoke-Command -VMName $VMName -Credential $Credentials -ScriptBlock {
        Import-Module -Name "NTS-ConfigMgrTools"
        return (Test-RebootPending)
    }
    if ($RebootPending) {
        Write-Output "$($env:COMPUTERNAME): doing an reboot of $($VMName)"
        Restart-VM -VM $VM -Type Reboot -Force -Wait
        Confirm-VMState -VMObject $VM -VMCredential $Credentials
    }
}

function Build-VMHost {
    <#
        .Description
        this function checks if the vm has to reboot and does it, if needed
 
        .Parameter Course_Shortcut
        shortcut of the participant, used for the switchname
 
        .Parameter TrustedHostsValue
        value for trustedhosts to add, needed for configmgr things
 
        .Parameter VM_Drive_Letter
        letter for the vm volume, which will be created
 
        .Example
        # this prepared the host and creates vm switch, vm volume
        Build-VMHost -Course_Shortcut $Course_Shortcut -TrustedHostsValue "$($CM_Siteserver_NetBIOS),$($CM_Siteserver_FQDN)"
 
        .NOTES
         
    #>


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

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

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

    $Host_Preparations_Start = Get-Date
    try {
        Write-Output "`n$($env:COMPUTERNAME): prepare host"
        
        # configmgr
        Start-Service -Name "WinRM"
        Set-Item -Path "WSMan:\localhost\Client\TrustedHosts" -Value $TrustedHostsValue -Force -Concatenate

        # prepare host for vms
        New-VMVolume -VMDriveLetter $VM_Drive_Letter
        New-VMVSwitch -Course_Shortcut $Course_Shortcut

        # hyperv
        $VM_DefaultPath = "$($VM_Drive_Letter):\VMs"
        Set-VMHost -EnableEnhancedSessionMode $True -VirtualHardDiskPath $VM_DefaultPath -VirtualMachinePath $VM_DefaultPath
    }
    catch {
        throw "$($env:COMPUTERNAME): host preparations failed: $($PSItem.Exception.Message)"
    }
    $Host_Preparations_Duration = (Get-Date) - $Host_Preparations_Start
    Write-Output "$($env:COMPUTERNAME): host preparation took $($Host_Preparations_Duration.Hours)h $($Host_Preparations_Duration.Minutes)m $($Host_Preparations_Duration.Seconds)s"
}

function Deploy-VMs {
    <#
        .Description
        this function checks if the vm has to reboot and does it, if needed
 
        .Parameter VM_Config_Obj
        Object that contains multiple descriptive objects for deployment from a VM.
 
        .Parameter Course_Shortcut
        shortcut of the participant
 
        .Parameter CM_Siteserver_FQDN
        fqdn of siet server
 
        .Parameter CM_Credentials
        credentials to connect to the config
 
        .Parameter VM_Credentials
        admin credentials to connect to the vm
 
        .Example
        # this does a restart of the vm if needed
        Deploy-VMs -VM_Config_Obj $VM_Config -Course_Shortcut $Course_Shortcut -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -VM_Credentials $VM_Credentials
 
        .NOTES
 
    #>


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

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

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

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

        [Parameter(Mandatory = $true)]
        [pscredential]
        $VM_Credentials
    )
    
    $VM_Deployment_Start = Get-Date
    try {
        Write-Output "`n$($env:COMPUTERNAME): starting vm deployments"
        New-VMs_Objectbased -VM_Config $VM_Config_Obj -Course_Shortcut $Course_Shortcut
        Write-Output "`n$($env:COMPUTERNAME): starting vm registration in configmgr"
        Register-VM_in_CM -VM_Config_Obj $VM_Config_Obj -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials
        Write-Output "`n$($env:COMPUTERNAME): starting vms"
        Start-VM_Deployment -VM_Config_Obj $VM_Config_Obj
        Write-Output "`n$($env:COMPUTERNAME): validating vm deployments"
        Confirm-VM_Deployment -VM_Config_Obj $VM_Config_Obj -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials
        Write-Output "finished vm deployments - doing cleanup in configmgr"
        Remove-VMConfigMgrDeployment -VM_Config_Obj $VM_Config_Obj -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials
    }
    catch {
        throw "$($env:COMPUTERNAME): error deploying vms: $($PSItem.Exception.Message)"
    }

    $VM_Config_Obj.Keys | Sort-Object | ForEach-Object {
        Set-VMInstallSnapshot -VMName $PSItem -SnapshotName "$(Get-Date -format "yyyy-MM-dd_HH.mm.ss") - initial deployment" -VMCredential $VM_Credentials
    }

    $VM_Deployment_Duration = (Get-Date) - $VM_Deployment_Start
    Write-Output "$($env:COMPUTERNAME): vm deployment took $($VM_Deployment_Duration.Hours)h $($VM_Deployment_Duration.Minutes)m $($VM_Deployment_Duration.Seconds)s"
}

function Remove-VMConfigMgrDeployment {
    <#
        .Description
        Removes the ConfigMgr Object related to the vms
 
        .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
        # Removes all vm objects in configmgr
        Remove-VMConfigMgrDeployment -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
    )
    
    Invoke-Command -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials -ScriptBlock {
        $VM_Config_Obj = $using:VM_Config_Obj
        $PSDriveName = "CM-Temp-Drive"

        # 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):\"
        
        # remove collections membership rules
        $CM_SiteCode = (Get-CMSite).SiteCode
        $VM_Config_Obj.Keys | ForEach-Object {
            $Temp_CMDevice = Get-CMDevice -Name $PSItem
            $TargetCollection = Get-CMCollection -Name $($VM_Config_Obj.$PSItem.CM_Collection_Name)
            if ($null -ne $Temp_CMDevice) {
                $Collections = Get-CimInstance -Namespace "root/Sms/site_$($CM_SiteCode)" -ClassName "SMS_FullCollectionMembership" -Filter "ResourceID = $($Temp_CMDevice.ResourceID)" | `
                    Where-Object -Property CollectionID -eq $TargetCollection.CollectionID
                if ($null -ne $Collections) {
                    $Collections | ForEach-Object {
                        $MembershipRule = Get-CMCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID | Where-Object -Property RuleName -EQ $Temp_CMDevice.Name
                        if ($null -ne $MembershipRule) {
                            Write-Output "$($Temp_CMDevice.Name): removing collection membership in $((Get-CMCollection -Id $PSItem.CollectionID).Name) after the deployment"
                            Remove-CMDeviceCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID -ResourceId $MembershipRule.ResourceId -Confirm:$false -Force
                        }
                    }
                }
            }
        }

        # removing device object after the deployment
        $VM_Config_Obj.Keys | Sort-Object | ForEach-Object {
            $Temp_CMDevice = Get-CMDevice -Name $PSItem
            if ($null -ne $Temp_CMDevice) {
                Write-Output "$($PSItem): removing computer info in configmgr after the deployment"
                Remove-CMDevice -Name $PSItem -Force -Confirm:$false
            }
        }
    }
}

function New-VMDiskFormated {
    <#
        .Description
        this function adds a disk to the vm and formats
 
        .Parameter VMName
        name of vm
 
        .Parameter VolumeDriveLetter
        letter for volume
 
        .Parameter VolumeFriendlyName
        volume label
 
        .Parameter VolumeSize
        size in bytes
 
        .Example
        # adds a disk with the size of $ContentLibDisk_Size and formats it with letter L
        New-VMDiskFormated -VMName $PSItem -VolumeDriveLetter "L" -VolumeFriendlyName "ContentLib" -VolumeSize $ContentLibDisk_Size
 
        .NOTES
         
    #>


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

        [Parameter(Mandatory = $true)]
        [char]
        $VolumeDriveLetter,

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

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

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

    $VM = Get-VM -Name $VMName
    $CurrentVMDiskCount = (Get-VHD -VMId $VM.Id).count
    
    $VolumeExists = Invoke-Command -VMName $VM.Name -Credential $VMCredential -ScriptBlock {                
        if ($null -eq (Get-Volume -DriveLetter $using:VolumeDriveLetter -ErrorAction SilentlyContinue)) {
            return $false
        }
        else {
            $true
        }
    }
    if ($VolumeExists -eq $false) {
        Add-VMDisk -VMName $VM.Name -VHDXPath "$($VM.ConfigurationLocation)\$($VM.Name)-$($CurrentVMDiskCount).vhdx" -VHDXSize $VolumeSize
        Invoke-Command -VMName $VM.Name -Credential $VMCredential -ScriptBlock {
            Write-Output "$($env:COMPUTERNAME): formating disks"
            $PhysicalDisk = Get-PhysicalDisk | Where-Object -Property Size -eq $using:VolumeSize
            New-Volume -DiskNumber $PhysicalDisk.DeviceId -FriendlyName $using:VolumeFriendlyName -FileSystem NTFS -DriveLetter $using:VolumeDriveLetter | Out-Null
        }
    }
}

function Get-ExchangeCU {
    <#
        .Description
        downloads exchange cu from microsoft
 
        .Parameter Version
        version of the cu
 
        .Parameter Outpath
        path where the cu is stored
 
        .Example
        # stores the cu to $Outpath
        Get-ExchangeCU -Version 2019_CU12 -Outpath $Outpath
 
        .NOTES
        https://learn.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates?view=exchserver-2019
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("2019_CU12", "2016_CU23")]
        [string]
        $Version,

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

    $ErrorActionPreference = 'Stop'
    if ($Outpath[-1] -eq "\") {
        $Outpath = $Outpath.Substring(0, $Outpath.Length - 1)
    }
    $ExchangeCUFilePath = "$($Outpath)\Exchange-$($Version).iso"

    switch ($Version) {
        "2019_CU12" { $DownloadURL = "https://www.microsoft.com/en-us/download/confirmation.aspx?id=104131" }
        "2016_CU23" { $DownloadURL = "https://www.microsoft.com/en-us/download/confirmation.aspx?id=104132" }
        Default { throw "no version was selected or not supported" }
    }

    try {
        $Content = Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL
        $UpdateLink = ($Content.Links | Where-Object -FilterScript { $PSItem.href -like "*download.microsoft.com*" -and $PSItem.outerHTML -like "*download manually*" }).href

        Start-FileDownload -DownloadURL $UpdateLink -FileOutPath $ExchangeCUFilePath
    }
    catch {
        throw "$($env:COMPUTERNAME): error getting exchange cu files - $($PSItem.Exception.Message)"
    }

    Write-Output "$($env:COMPUTERNAME): finished download - check folder $($Outpath)"
}

function Start-FileDownload {
    <#
        .Description
        this function can be used to download files, but also checks if the destination has already the file
 
        .Parameter DownloadURL
        url of the source
 
        .Parameter FileOutPath
        path where the file should be saved, with extension
 
        .Parameter MaxAgeOfFile
        maximum file modification date
 
        .Example
        # downloads the file
        Start-FileDownload -DownloadURL "https://www.microsoft.com/en-us/download/confirmation.aspx?id=104131" -FileOutPath "$($Outpath)\Exchange-$($Version).iso"
 
        .NOTES
 
    #>


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

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

        [Parameter(Mandatory = $false)]
        [datetime]
        $MaxAgeOfFile = (Get-Date).AddHours(-2)
    )
    
    # download
    try {
        if ((Test-Path -Path $FileOutPath) -eq $true) {
            if ((Get-Item $FileOutPath).LastWriteTime -gt $MaxAgeOfFile) {
                Write-Output "$($env:COMPUTERNAME): found $($FileOutPath), will use it"
            } 
            else {
                Write-Output "$($env:COMPUTERNAME): found $($SetupName) at $($FileOutPath), removing the files because too old"
                Remove-Item -Path $FileOutPath -Recurse -Force | Out-Null
            }
        }
        Write-Output "$($env:COMPUTERNAME): downloading from $($DownloadURL) to $($FileOutPath)"
        $ProgressPreference = "SilentlyContinue"
        Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL -OutFile $FileOutPath
        $ProgressPreference = "Continue"
        Write-Output "$($env:COMPUTERNAME): download finished"
    }
    catch {
        throw "error downloading - $($PSItem.Exception.Message)"
    }
}