NTS.Tools.MSHyperV.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-Volume -DriveLetter $VMDriveLetter -ErrorAction SilentlyContinue)) {
            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"
                }
    
                if ($null -ne $NVMe_Devices -and ($SelectedDisks | Measure-Object).Count) {
                    Initialize-Disk -Number $SelectedDisks.DeviceId -PartitionStyle GPT | Out-Null
                    New-Volume -DiskNumber $SelectedDisks.DeviceId -FriendlyName $VMVolumeName -FileSystem ReFS -DriveLetter $VMDriveLetter | Out-Null
                }
                else {
                    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"
            }
        }
        else {
            Write-Output "$($env:COMPUTERNAME): volume $($VMDriveLetter) 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 Name
        Name of the VM Switch
 
        .Example
        # Creates a new VM Switch with the name IC
        New-VMVSwitch -Name 'IC'
 
        .NOTES
        there must be at least one nic with status 'up'
    #>


    param (
        # Course Shortcut
        [Parameter(Mandatory = $false)]
        [string]
        $Name = "LAN"
    )
    
    try {
        if ($null -eq (Get-VMSwitch -Name $Name -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 $($Name)"
            New-VMSwitch -Name $Name -NetAdapterName $Selected_NIC.Name -AllowManagementOS $false | Out-Null
            Add-VMNetworkAdapter -ManagementOS -SwitchName $Name -Name "vNIC-$($Name)"
            Rename-NetAdapter -Name $Selected_NIC.Name -NewName "pNIC-$($Name)"
        }
        else {
            Write-Output "$($env:COMPUTERNAME): virtual vswitch $($Name) 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
 
        .Parameter SecondsToWait
        Seconds to wait after the registration in configmgr finished
 
        .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,

        [Parameter(Mandatory = $false)]
        [int]
        $SecondsToWait = (Get-Random -Minimum 45 -Maximum 90)
    )

    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
            $CMPSDriveName = "CMPS-$(Get-Random)"
            $CM_Collection_All_Systems_ID = "SMS00001"

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

            $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

            # checking existing devices
            try {
                $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
                    }
                }
            }
            catch {
                throw "error checking/removing device in configmgr - $($PSItem.Exception.Message)"
            }

            # check destination collection existance
            try {
                $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"
                    }
                }
            }
            catch {
                throw "error checking collection in configmgr - $($PSItem.Exception.Message)"
            }

            Start-Sleep -Seconds 10
            
            # import device
            try {
                $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
                    }
                }
            }
            catch {
                throw "error adding device in configmgr - $($PSItem.Exception.Message)"
            }

            # add device to target collection
            try {
                # 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
                }
            }
            catch {
                throw "error adding device to target collection - $($PSItem.Exception.Message)"
            }

            # remove collections, so there is only the targeted
            try {
                $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
                                }
                            }
                        }
                    }
                }
            }
            catch {
                throw "error removing additional collections - $($PSItem.Exception.Message)"
            }

            Set-Location -Path $env:SystemDrive
            Remove-PSDrive -Name $CMPSDriveName
        }
        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_PXEDeployment {
    <#
        .Description
        Starts VMs
 
        .Parameter VM_Config_Obj
        Object that contains multiple descriptive objects for deployment from a VM.
 
        .Parameter DelayStartsInSeconds
        delays the start of vms for the amount of seconds
 
        .Example
        # Starts VMs, based on objects in $VM_Config
        Start-VM_PXEDeployment -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 = $true)]
        [hashtable]
        $VM_Config_Obj,

        [Parameter(Mandatory = $false)]
        [int]
        $DelayStartsInSeconds = (Get-Random -Minimum 3 -Maximum 5)
    )

    try {
        Write-Verbose "$($env:COMPUTERNAME): starting vms for pxe deployment, using delay of $($DelayStartsInSeconds)"
        $VM_Config_Obj.Keys | Sort-Object | ForEach-Object {
            Write-Output "$($env:COMPUTERNAME): starting vm $($PSItem)"
            Start-Sleep -Seconds $DelayStartsInSeconds
            Start-VM -VMName $PSItem
        }
    }
    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
 
        .Parameter VM_Credentials
        admin credentials to connect to the vm
 
        .Parameter TimeoutInMinutes
        time out in minitues, when to stop checking the deploymentstatus
 
        .Parameter DisableAutoClearPXE
        disables the pxe flag clear functionality
 
        .Parameter PXEClearAfterInSeconds
        defines how many seconds to wait until the pxe flag should be cleared
 
        .Parameter WaitForCMDatabaseThresholdInSeconds
        defines the amount of seconds to wait for data in configmgr db
 
        .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 = $true)]
        [hashtable]
        $VM_Config_Obj,

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

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

        [Parameter(Mandatory = $true)]
        [PSCredential]
        $VM_Credentials, 

        [Parameter(Mandatory = $false)]
        [int]
        $TimeoutInMinutes = 120,

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

        [Parameter(Mandatory = $false)]
        [int]
        $PXEClearAfterInSeconds = 120,

        [Parameter(Mandatory = $false)]
        [int]
        $WaitForCMDatabaseThresholdInSeconds = 400
    )

    # PS Session
    try {
        Confirm-VMPresence -VM_Config_Obj $VM_Config_Obj
        $CMPS_Session = New-PSSession -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials
        Invoke-Command -Session $CMPS_Session -ScriptBlock {
            $CMPSDriveName = "CMPS-$(Get-Random)"

            Import-Module -Name "ConfigurationManager"
            New-PSDrive -Name $CMPSDriveName -PSProvider "CMSite" -Root $using:CM_Siteserver_FQDN -Description "Primary site" | Out-Null
            Set-Location -Path "$($CMPSDriveName):\"
        }
    }
    catch {
        throw "error ps session to $($CM_Siteserver_FQDN): $($PSItem.Exception.Message)"
    }

    # check status
    try {
        $Deployment_Finished = $false
        $StartDate = Get-Date
        $CM_Deployment_Running = @{}
        $DeploymentProperties = @{}

        # set environment
        try {
            $VM_Config_Obj.Keys | Sort-Object | ForEach-Object {
                $CM_Deployment_Running.Add($PSItem, $true)
    
                $ResourceID = Invoke-Command -Session $CMPS_Session -ArgumentList $PSItem -ScriptBlock {
                    try {
                        return (Get-CMDevice -Name $args[0] -Fast).ResourceID
                    }
                    catch {
                        throw "$($PSItem.Exception.Message)"
                    }
                }
                $DeploymentID = Invoke-Command -Session $CMPS_Session -ArgumentList $($VM_Config_Obj).$PSItem.CM_Collection_Name -ScriptBlock {
                    try {
                        return (Get-CMDeployment -CollectionName $args[0]).DeploymentID
                    }
                    catch {
                        throw "$($PSItem.Exception.Message)"
                    }
                }
                $TaskSequenceName = Invoke-Command -Session $CMPS_Session -ArgumentList $($VM_Config_Obj).$PSItem.CM_Collection_Name -ScriptBlock {
                    try {
                        return (Get-CMDeployment -CollectionName $args[0]).SoftwareName
                    }
                    catch {
                        throw "$($PSItem.Exception.Message)"
                    }
                }
                
                $Properties = @{
                    ResourceID             = $ResourceID
                    DeploymentID           = $DeploymentID
                    TaskSequenceName       = $TaskSequenceName
                    PXEFlagCleared         = $false
                    FoundDataInConfigMgrDB = $false
                }
    
                $DeploymentProperties.Add($PSItem, $Properties)
            }
        }
        catch {
            throw "could not set environment - $($PSItem.Exception.Message)"
        }
        
        $VM_Config.Keys | Sort-Object | ForEach-Object {
            if ($($DeploymentProperties).$PSItem.PXEFlagCleared -ne $true) {
                Write-Output "$($PSItem): the task sequence did not start, because no data was found in configmgr db, try to reset pxe flag and restart vm once"
            }
            else {
                Write-Output "$($PSItem): shit"
            }
        }

        Write-Output "---"
        $VM_Config_Obj.keys | Sort-Object | ForEach-Object {
            Write-Output "$($PSItem): the task sequence $($($DeploymentProperties).$PSItem.TaskSequenceName) for the os deployment is used"
        }
        Write-Output "---"

        # check status of deployment
        do {
            $Duration = (Get-Date) - $StartDate
            $VM_Config_Obj.keys | Sort-Object | ForEach-Object {
                $StatusMessages = $null
                $ResourceID = $($DeploymentProperties).$PSItem.ResourceID
                $DeploymentID = $($DeploymentProperties).$PSItem.DeploymentID
                $StatusMessages = Invoke-Command -Session $CMPS_Session -ArgumentList ($ResourceID, $DeploymentID) -ScriptBlock {
                    try {
                        $CM_SiteCode = (Get-CMSite).SiteCode
                        $Query = "Select AdvertisementID,ResourceID,Step,ActionName,LastStatusMessageIDName from v_TaskExecutionStatus where (AdvertisementID = '$($args[1])' AND ResourceID = '$($args[0])')"
                        return (Invoke-Sqlcmd -ServerInstance "$($using:CM_Siteserver_FQDN)\$($CM_SiteCode)" -Database "CM_$($CM_SiteCode)" -Query $Query | Sort-Object -Property Step -Descending)
                    }
                    catch {
                        throw "could not get data from db - $($PSItem.Exception.Message)"
                    }
                }

                # output status
                try {
                    if ($null -eq $StatusMessages -or $StatusMessages -eq "") {
                        Write-Output "$($PSItem): waiting on data in configmgr database, $($Duration.TotalSeconds) seconds since start, thresholds are $($PXEClearAfterInSeconds) and $($WaitForCMDatabaseThresholdInSeconds)"
                    }
                    else {
                        $($DeploymentProperties).$PSItem.FoundDataInConfigMgrDB = $true
                        $StatusObject = @{
                            "DeviceName"              = $PSItem
                            "AdvertisementID"         = $StatusMessages[0].AdvertisementID
                            "ResourceID"              = $StatusMessages[0].ResourceID
                            "Step"                    = $StatusMessages[0].Step
                            "ActionName"              = $StatusMessages[0].ActionName
                            "LastStatusMessageIDName" = $StatusMessages[0].LastStatusMessageIDName
                        }
    
                        #region set actioname if empty
                        if ($StatusMessages[0].ActionName -ne "") {
                            $StatusObject.ActionName = $StatusMessages[0].ActionName
                        }
                        elseif ($StatusMessages[1].ActionName -ne "") {
                            $StatusObject.ActionName = $StatusMessages[1].ActionName
                        }
                        elseif ($StatusMessages[2].ActionName -ne "") {
                            $StatusObject.ActionName = $StatusMessages[2].ActionName
                        }
                        elseif ($StatusMessages[3].ActionName -ne "") {
                            $StatusObject.ActionName = $StatusMessages[3].ActionName
                        }
                        elseif ($StatusMessages[4].ActionName -ne "") {
                            $StatusObject.ActionName = $StatusMessages[4].ActionName
                        }
                        elseif ($StatusMessages[5].ActionName -ne "") {
                            $StatusObject.ActionName = $StatusMessages[5].ActionName
                        }
                        elseif ($StatusMessages[6].ActionName -ne "") {
                            $StatusObject.ActionName = $StatusMessages[6].ActionName
                        }
                        else {
                            $StatusObject.ActionName = "no actionname"
                        }
                        #endregion
    
                        if ($StatusObject.ActionName -like "*Final Restart*" -or `
                                $StatusObject.LastStatusMessageIDName -like "*The task sequence manager successfully completed execution of the task sequence*" -or `
                                $StatusObject.LastStatusMessageIDName -like "*The task execution engine successfully completed a task sequence*"
                        ) {
                            $CM_Deployment_Running.$($PSItem) = $false
                        }
                        
                        if ($CM_Deployment_Running.$($PSItem) -eq $false) {
                            Write-Output "$($StatusObject.DeviceName): finished"
                        }
                        else {
                            Write-Output "$($StatusObject.DeviceName): step $($StatusObject.Step) - $($StatusObject.ActionName): $($StatusObject.LastStatusMessageIDName)"
                        }
                    }
                }
                catch {
                    throw "could not generate output - $($PSItem.Exception.Message)"
                }

                # pxe clear after a period of time no boot was detected
                if (
                    $DisableAutoClearPXE -eq $false -and `
                        $Duration.TotalSeconds -ge $PXEClearAfterInSeconds -and `
                        $Duration.TotalSeconds -lt $WaitForCMDatabaseThresholdInSeconds -and `
                    $($DeploymentProperties).$PSItem.PXEFlagCleared -ne $true -and `
                    $($DeploymentProperties).$PSItem.FoundDataInConfigMgrDB -ne $true
                ) {
                    Write-Output "$($PSItem): the task sequence did not start, because no data was found in configmgr db, try to reset pxe flag and restart vm once"
                    Get-VM -Name $PSItem | Stop-VM -TurnOff -Force
                    Invoke-Command -Session $CMPS_Session -ArgumentList $PSItem -ScriptBlock {
                        try {
                            Get-CMDevice -Name $args[0] -Fast | Clear-CMPxeDeployment
                        }
                        catch {
                            throw "could not clear pxe flag - $($PSItem.Exception.Message)"
                        }
                    }
                    $($DeploymentProperties).$PSItem.PXEFlagCleared = $true
                    Start-Sleep -Seconds 7
                    Start-VM -Name $PSItem
                }

                # stops after timeout
                if ($Duration.TotalSeconds -ge $WaitForCMDatabaseThresholdInSeconds) {
                    throw "waited for $($WaitForCMDatabaseThresholdInSeconds) seconds, but no data for $($($StatusObject.DeviceName)) in cm database. verify osd/pxe started on the vm"
                }
                Start-Sleep -Seconds 1 # strange errors, session maybe broke
            }
            Write-Output "---"
            if ($CM_Deployment_Running.Values -notcontains $true) {
                $Deployment_Finished = $true
            }
            else {
                Start-Sleep -Seconds 15
            }
        } while ($Deployment_Finished -eq $false -and $Duration.TotalMinutes -le $TimeoutInMinutes)
    
        if ($Duration.TotalMinutes -ge $TimeoutInMinutes) {
            throw "deployment not finished after $($TimeoutInMinutes) mins, check the logs in configmgr or inside the vms"
            Write-Output "Duration $($Duration.TotalMinutes -ge $TimeoutInMinutes)"
        }

        Invoke-Command -Session $CMPS_Session -ScriptBlock {
            Set-Location -Path $env:SystemDrive
            Remove-PSDrive -Name $CMPSDriveName
        }
    
        Remove-PSSession -Session $CMPS_Session
    }
    catch {
        throw "error checking 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 SwitchName
        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 $SwitchName
        New-VMs_Objectbased -VM_Config $VM_Config -SwitchName $SwitchName
 
        .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 = ""
            vTPMEnabled = $true
            AutoStartEnabled = $false
        }
    #>


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

        [Parameter(Mandatory = $false)]
        [string]
        $VMPath = "V:\VMs",

        [Parameter(Mandatory = $true)]
        [string]
        $SwitchName
    )
    
    try {
        if ($VMPath[-1] -eq "\") {
            $VMPath = $VMPath.Substring(0, $VMPath.Length - 1)
        }
        foreach ($VM in $VM_Config_Obj.Keys | Sort-Object) {
            $VMVHDXPath = ($VMPath + "\" + $VM_Config_Obj.$VM.Name + "\" + $VM_Config_Obj.$VM.Name + ".vhdx")
            Write-Output "$($env:COMPUTERNAME): creating vm $($VM_Config_Obj.$VM.Name)"
            try {
                if (Test-Path -Path $VMVHDXPath) {
                    throw "vhdx for $($VM_Config_Obj.$VM.Name) already exists, please remove it"
                }

                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 $VMPath -Generation 2 -VHDPath $VMVHDXPath -BootDevice NetworkAdapter -SwitchName $SwitchName | Out-Null
            }
            catch {
                throw "error during creation of vhdx or vm ($($VM_Config_Obj.$VM.Name)) - $($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
                }
                if ($VM_Config_Obj.$VM.vTPMEnabled) {
                    Set-VMKeyProtector -VMName $VM_Config_Obj.$VM.Name -NewLocalKeyProtector
                    Enable-VMTPM -VMName $VM_Config_Obj.$VM.Name
                }
                if ($VM_Config_Obj.$VM.AutoStartEnabled) {
                    Set-VM -AutomaticStartAction Start -VMName $VM_Config_Obj.$VM.Name -AutomaticStartDelay 10 
                }
                else {
                    Set-VM -AutomaticStartAction Nothing -VMName $VM_Config_Obj.$VM.Name
                }
                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 "error while setting properties ($($VM_Config_Obj.$VM.Name)) - $($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 "$($env:COMPUTERNAME): 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 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
    )

    Write-Output "$($VMName): configuring network"
    Invoke-Command -VMName $VMName -Credential $VMCredential -ScriptBlock {
        try {
            $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 "$($env:COMPUTERNAME): 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
    )

    $ErrorActionPreference = 'Stop'

    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 {
            try {
                Confirm-DomainConnectivity -DomainName $using:DomainName
                Start-Sleep -Seconds 1
                if ($NoReboot -eq $true) {
                    if ($null -ne $OUPath) {
                        Add-Computer -Credential $using:DomainCredential -DomainName $using:DomainName -OUPath $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 $OUPath -Restart -WarningAction SilentlyContinue
                    }
                    else {
                        Add-Computer -Credential $using:DomainCredential -DomainName $using:DomainName -Restart -WarningAction SilentlyContinue
                    }
                }
            }
            catch {
                throw $PSItem.Exception.Message
            }
        }
        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 "error while connecting to $($VMObject.Name) with ps direct - $($PSItem.Exception.Message)"
        }    
    }
    catch {
        throw "$($env:COMPUTERNAME): $($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 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
    )
    
    try {
        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
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

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.Tools" -DisableNameChecking
        return (Test-RebootPending)
    }
    if ($RebootPending) {
        Write-Output "$($env:COMPUTERNAME): doing a reboot of $($VMName)"
        Restart-VM -VM $VM -Type Reboot -Force -Wait
        Confirm-VMState -VMObject $VM -VMCredential $Credentials
    }
}

function New-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()]
    [Alias("Build-VMHost")]
    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 -Name $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 Install-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 SwitchName
        shortcut of the participant
 
        .Parameter VMPath
        path where the vms files should be placed
 
        .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
 
        .Parameter SecondsToWaitBeforeCreatingSnapshots
        Seconds to wait before creating Snapshots
 
        .Parameter SnapShotNameSuffix
        string to add to the snapshot name at the end
 
        .Example
        # this does a restart of the vm if needed
        Deploy-VMs -VM_Config_Obj $VM_Config -SwitchName $SwitchName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -VM_Credentials $VM_Credentials
 
        .NOTES
 
    #>


    [CmdletBinding()]
    [Alias("Deploy-VMs")]
    param (
        [Parameter(Mandatory = $true)]
        [hashtable]
        $VM_Config_Obj,

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

        [Parameter(Mandatory = $false)]
        [string]
        $VMPath = "V:\VMs", 

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

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

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

        [Parameter(Mandatory = $false)]
        [int]
        $SecondsToWaitBeforeCreatingSnapshots = 120,
     
        [Parameter(Mandatory = $false)]
        [string]
        $SnapShotNameSuffix = "$(Get-Date -format "yyyy-MM-dd_HH.mm.ss") - initial deployment"
    )
    
    # checks
    try {
        $VM_Config_Obj.Keys | ForEach-Object {
            if ($null -ne (Get-VM -Name $VM_Config_Obj.$PSItem.Name -ErrorAction SilentlyContinue)) {
                throw "vm $($VM_Config_Obj.$PSItem.Name) already exists, stopping"
            }
        }
    }
    catch {
        throw "error doing prereq checks - $($PSItem.Exception.Message)"
    }

    # deployment
    $VM_Deployment_Start = Get-Date
    try {
        Write-Output "`n$($env:COMPUTERNAME): starting vm creation"
        New-VMs_Objectbased -VM_Config_Obj $VM_Config_Obj -VMPath $VMPath -SwitchName $SwitchName
        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 vm pxe deployment"
        Start-VM_PXEDeployment -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 -VM_Credentials $VM_Credentials
        Write-Output "$($env:COMPUTERNAME): now waiting for $($SecondsToWaitBeforeCreatingSnapshots) seconds"
        Start-Sleep -Seconds $SecondsToWaitBeforeCreatingSnapshots
        Write-Output "$($env:COMPUTERNAME): 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)"
    }

    # snapshots
    $VM_Config_Obj.Keys | Sort-Object | ForEach-Object {
        Set-VMInstallSnapshot -VMName $PSItem -SnapshotName "$($PSItem) - $($SnapShotNameSuffix)" -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
        $CMPSDriveName = "CMPS-$(Get-Random)"

        # load configmgr ps module
        Import-Module -Name "ConfigurationManager"
        New-PSDrive -Name $CMPSDriveName -PSProvider "CMSite" -Root $using:CM_Siteserver_FQDN -Description "Primary site" | Out-Null
        Set-Location -Path "$($CMPSDriveName):\"
        
        # remove collections membership rules
        try {            
            $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
                            }
                        }
                    }
                }
            }
        }
        catch {
            throw "error removing collection membership rules - $($PSItem.Exception.Message)"
        }

        # removing device object after the deployment
        try {            
            $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
                }
            }
        }
        catch {
            throw "error removing device - $($PSItem.Exception.Message)"
        }

        Set-Location -Path $env:SystemDrive
        Remove-PSDrive -Name $CMPSDriveName
    }
}

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,

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

        [Parameter(Mandatory = $false)]
        [ValidateSet("NTFS", "REFS")]
        [string]
        $FileSystem = "NTFS"
    )

    try {
        $VM = Get-VM -Name $VMName
        $CurrentVMDiskCount = (Get-VHD -VMId $VM.Id).count
        if ($null -eq $VHDXPathXPath -or $VHDXPathXPath -eq "") {
            $VHDXPathXPath = "$($VM.ConfigurationLocation)\$($VM.Name)-$($CurrentVMDiskCount).vhdx"
        }
    }
    catch {
        throw "could not find out the vm or the number of disks present - $($PSItem.Exception.Message)"
    }
    
    try {
        $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) {
            Write-Output "$($env:COMPUTERNAME): adding disk to $($VM.Name)"
            Add-VMDisk -VMName $VM.Name -VHDXPath $VHDXPathXPath -VHDXSize $VolumeSize
            Invoke-Command -VMName $VM.Name -Credential $VMCredential -ScriptBlock {
                try {
                    Write-Output "$($env:COMPUTERNAME): formating disk"
                    $PhysicalDisk = Get-PhysicalDisk | Where-Object -Property Size -eq $using:VolumeSize | Sort-Object -Property DeviceId
                    if ($PhysicalDisk.Count -gt 1) {
                        $Disk = ($PhysicalDisk | ForEach-Object {
                                Get-Disk -Number $PSItem.DeviceId | Where-Object -Property PartitionStyle -eq "RAW"
                            })[0]
                        New-Volume -DiskNumber $Disk.Number -FriendlyName $using:VolumeFriendlyName -FileSystem $using:FileSystem -DriveLetter $using:VolumeDriveLetter | Out-Null
                    }
                    else {
                        New-Volume -DiskNumber $PhysicalDisk.DeviceId -FriendlyName $using:VolumeFriendlyName -FileSystem $using:FileSystem -DriveLetter $using:VolumeDriveLetter | Out-Null
                    }
                }
                catch {
                    throw $PSItem.Exception.Message
                }
            }
        }
        else {
            Write-Output "$($VM.Name): volume already exits"
        }
    }
    catch {
        throw "could not format volume - $($PSItem.Exception.Message)"
    }
}

function Connect-CourseVMToNetwork {
    <#
    .SYNOPSIS
        connects the vms with a course room
 
    .DESCRIPTION
        configures the vm network adapter for a specific course room, based on a hyper-v switch and vlan settings
 
    .PARAMETER VMFolderName
        Name of the folder under $($env:SystemDrive)\ClusterStorage\VMs where the vms are located
 
    .PARAMETER CourseRoom
        Course room name
 
    .PARAMETER ShowOnly
        only display changes, but dont make the changes
 
    .EXAMPLE
        Connect-CourseVMToNetwork -VMFolderName PKI -CourseRoom '4OG(Oben)' -ShowOnly
        shows only the possible change of the vmnetwork types configuration for the vms in PKI
 
    .EXAMPLE
        Connect-CourseVMToNetwork -VMFolderName PKI -CourseRoom '4OG(Oben)'
        connects all VMs in Folder "PKI" to the specified course room
 
    .NOTES
        - the vm files must be in the course folder under the $($env:SystemDrive)\ClusterStorage\VMs
        - all vlans available can be found at "https://netbox.ntsystems.de/ipam/vlans/"
        - which vlans are configure at "https://wlan.ntsystems.de/manage/default/devices"
        - vlan 10,190,403 will be skipped and therefore not changed
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = "Default", Position = 2)]
        [ValidateSet(
            "4OG(Oben)",
            "EG2(GROSS)",
            "EG3(KLEIN)",
            "VSCHULUNG1",
            "VSCHULUNG2",
            "NTSInstall"
        )]
        [string]
        $CourseRoom,

        [Parameter(Mandatory = $false, ParameterSetName = "Default", Position = 3)]
        [switch]
        $ShowOnly
    )

    dynamicParam {
        # Set the dynamic parameters' name
        $ParameterName = 'VMFolderName'
        
        # Create and set the parameters' attributes
        $ParameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute
        $ParameterAttribute.Mandatory = $true
        $ParameterAttribute.Position = 0
        $ParameterAttribute.ParameterSetName = "Default"

        # Create the collection of attributes
        $AttributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]

        # Add the attributes to the attributes collection
        $AttributeCollection.Add($ParameterAttribute)

        # Generate and set the ValidateSet
        $arrSet = (Get-ChildItem -Path "$($env:SystemDrive)\ClusterStorage\VMs" | Where-Object -Property Name -NotLike "_*").Name
        $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)

        # Add the ValidateSet to the attributes collection
        $AttributeCollection.Add($ValidateSetAttribute)

        # Create and return the dynamic parameter
        $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)

        # Create the dictionary
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)

        return $RuntimeParameterDictionary
    }

    begin {
        function Show-CurrentVNICConfig {
            [CmdletBinding()]
            param (
                [Parameter(Mandatory = $true)]
                [System.Array]
                $VMs
            )
            try {
                $VMs | ForEach-Object {
                    Get-VMNetworkAdapter -VM $PSItem | Select-Object -Property VMName, Name, SwitchName, MacAddress, @{label = "VLANOperationMode"; expression = { $PSItem.VlanSetting.OperationMode } }, @{label = "AccessVlanId"; expression = { $PSItem.VlanSetting.AccessVlanId } }
                } | Format-Table -AutoSize
            }
            catch {
                throw $PSItem.Exception.Message
            }
        }

        # static variabels
        $VMFolderName = $PsBoundParameters[$ParameterName]
        $ErrorActionPreference = 'Stop'
        $ConsoleHighlightColor = "Green"

        # vmswith and vlans
        $VMSwitch = "SCHULUNG"
        switch ($CourseRoom) {
            "4OG(Oben)" { $VLANID = "191" }
            "EG2(GROSS)" { $VLANID = "192" }
            "EG3(KLEIN)" { $VLANID = "193" }
            "VSCHULUNG1" { $VLANID = "194" }
            "VSCHULUNG2" { $VLANID = "195" }
            "NTSInstall" { $VLANID = "Untagged" }
            Default {
                throw "$($CourseRoom) was not found"
            }
        }
        $VLANsThatShouldNotBeChanged = @(
            "10", # EXTERN
            "190", # SCHULUNG-DMZ
            "403"  # NTSRouting
        )

        # collect VM info
        try {
            $VMs = Get-VM | Where-Object -Property Path -like "C:\ClusterStorage\VMs\$($VMFolderName)\*"
        }
        catch {
            throw "could not fetch vms - $($PSItem.Exception.Message)"
        }
    }
    process {
        # get settings
        Write-Host "`ncurrent vm netadapter configuration" -ForegroundColor $ConsoleHighlightColor
        Show-CurrentVNICConfig -VMs $VMs

        Confirm-Question -Question "do you want to proceed?"

        # set settings
        if ($ShowOnly) {
            Write-Host "`nshowing change that would apply" -ForegroundColor $ConsoleHighlightColor
        }
        else {
            Write-Host "`nvm netadapter configuration after change" -ForegroundColor $ConsoleHighlightColor
            Show-CurrentVNICConfig -VMs $VMs
        }

        $VMs | ForEach-Object {
            $VMNetadapters = Get-VMNetworkAdapter -VM $PSItem
            foreach ($Adapter in $VMNetadapters) {
                if ($VLANsThatShouldNotBeChanged -contains $Adapter.VlanSetting.AccessVlanId) {
                    Write-Verbose "$($Adapter.VMName): skipping adapter $($Adapter.Name) because its currently connected to VLAN $($Adapter.VlanSetting.AccessVlanId)"
                }
                elseif ($ShowOnly) {
                    Write-Output "$($Adapter.VMName): adapter $($Adapter.Name) will be connected to $($VMSwitch) using VLAN $($VLANID)"
                }
                else {
                    "---"
                    try {
                        Connect-VMNetworkAdapter -VMNetworkAdapter $Adapter -SwitchName $VMSwitch
                        Write-Verbose "$($Adapter.VMName): adapter $($Adapter.Name) now connected to $($VMSwitch)"
                    }
                    catch {
                        throw "could not connect vnic to vswitch - $($PSItem.Exception.Message)"
                    }
                    try {
                        if ($VLANID -eq "Untagged") {
                            Set-VMNetworkAdapterVlan -VMNetworkAdapter $Adapter -Untagged
                        }
                        else {
                            Set-VMNetworkAdapterVlan -VMNetworkAdapter $Adapter -Access -VlanId $VLANID
                        }
                        Write-Verbose "$($Adapter.VMName): adapter $($Adapter.Name) is using vlan $($VLANID) now"
                    }
                    catch {
                        throw "could not set vlan to vnic - $($PSItem.Exception.Message)"
                    }
                }
            }
        }

        # get settings
        if (!($ShowOnly)) {
            Write-Host "`nconfiguration after change" -ForegroundColor $ConsoleHighlightColor
            Show-CurrentVNICConfig -VMs $VMs
        }
    }
}

function Restore-CourseVMToLastSnapshot {
    <#
    .SYNOPSIS
        restores latest snapshots of course vms
 
    .DESCRIPTION
        restores latest snapshots of course vms
 
    .PARAMETER VMFolderName
        Name of the folder under $($env:SystemDrive)\ClusterStorage\VMs where the vms are located
 
    .PARAMETER ShowOnly
        only display changes, but dont make the changes
 
    .EXAMPLE
        Restore-CourseVMToLastSnapshot -VMFolderName PKI -ShowOnly
        shows only the possible snapshot which should be applied to vms in folder pki
 
    .EXAMPLE
        Restore-CourseVMToLastSnapshot -VMFolderName PKI
        applies the latest snapshot to vms of folder pki
 
    .NOTES
        - the vm files must be in the course folder under the $($env:SystemDrive)\ClusterStorage\VMs
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = "Default", Position = 2)]
        [switch]
        $ShowOnly
    )

    dynamicParam {
        # Set the dynamic parameters' name
        $ParameterName = 'VMFolderName'
        
        # Create and set the parameters' attributes
        $ParameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute
        $ParameterAttribute.Mandatory = $true
        $ParameterAttribute.Position = 1
        $ParameterAttribute.ParameterSetName = "Default"

        # Create the collection of attributes
        $AttributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]

        # Add the attributes to the attributes collection
        $AttributeCollection.Add($ParameterAttribute)

        # Generate and set the ValidateSet
        $arrSet = (Get-ChildItem -Path "$($env:SystemDrive)\ClusterStorage\VMs" | Where-Object -Property Name -NotLike "_*").Name
        $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)

        # Add the ValidateSet to the attributes collection
        $AttributeCollection.Add($ValidateSetAttribute)

        # Create and return the dynamic parameter
        $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)

        # Create the dictionary
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)

        return $RuntimeParameterDictionary
    }

    begin {
        # static variabels
        $VMFolderName = $PsBoundParameters[$ParameterName]
        $ErrorActionPreference = 'Stop'
        $ConsoleHighlightColor = "Green"

        # collect VM info
        try {
            $VMs = Get-VM | Where-Object -Property Path -like "C:\ClusterStorage\VMs\$($VMFolderName)\*"
        }
        catch {
            throw "could not fetch vms - $($PSItem.Exception.Message)"
        }
    }
    process {
        # get settings
        try {
            $AllVMSnapshots = Get-VMSnapshot -VM $VMs
            $SelectedSnapshots = foreach ($VM in $VMs) {
                $CurrentVMSnapshots = $AllVMSnapshots | Where-Object -Property VMName -eq $VM.Name
                $SelectedCurrentVMSnapshots = $CurrentVMSnapshots | Sort-Object -Property CreationTime -Descending
                if ($null -ne $SelectedCurrentVMSnapshots) {
                    $SelectedCurrentVMSnapshots[0]
                }
                else {
                    Write-Verbose "no snapshot found for $($VM.Name)"
                }
            }
        }
        catch {
            throw "could not get vm snapshots - $($PSItem.Exception.Message)"
        }

        # verify snapshot counts
        foreach ($VM in $VMs) {
            $CurrentVMSnapshots = $AllVMSnapshots | Where-Object -Property VMName -eq $VM.Name
            if ($CurrentVMSnapshots.count -ge 2) {
                Write-Verbose "found more than 1 snapshot for $($VM.Name)"
            }
            if ($CurrentVMSnapshots.count - 0) {
                Write-Verbose "found no snapshot for $($VM.Name)"
            }
        }

        Write-Host "`nfound the following snapshots" -ForegroundColor $ConsoleHighlightColor
        $AllVMSnapshots | Sort-Object -Property VMName, CreationTime | Format-Table -AutoSize

        Write-Host "`nthe following snapshots will be used to restore the vm state, if no snapshot was found then the vm will keep its state" -ForegroundColor $ConsoleHighlightColor
        $SelectedSnapshots | Sort-Object -Property VMName, CreationTime | Format-Table -AutoSize

        if (-not $ShowOnly) {
            Confirm-Question -Question "do you want to proceed?"
            $SelectedSnapshots | ForEach-Object {
                try {
                    Write-Verbose "apply snapshot $($PSItem.Name) to vm $($PSItem.VMName)"
                    Restore-VMCheckpoint -VMSnapshot $PSItem -Confirm:$False
                }
                catch {
                    throw "could not apply snapshot $($PSItem.Name) to vm $($PSItem.VMName) - $($PSItem.Exception.Message)"
                }
            }
        }
    }
}

function Reset-VMDeployment {
    <#
        .Description
        this function can be used to reset course vm deployment
 
        .Parameter Type
        type of reset
 
        .Parameter SnapShotNamePattern
        pattern for the snapshot name
 
        .Parameter VMFilesPath
        path to vm files
 
        .Parameter Force
        switch for not asking to do stuff
 
        .Example
        # removes all vms and the files
        Reset-VMDeployment -Type Remove -Force
 
        .Example
        # sets vms to snapshot with name like "initial"
        Reset-VMDeployment -Type ToSnapshot -SnapShotNamePattern "initial"
 
        .NOTES
         
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("Remove", "ToSnapshot")]
        [string]
        $Type,

        [Parameter(Mandatory = $true, ParameterSetName = "ToSnapshot")]
        [string]
        $SnapShotNamePattern,


        [Parameter(Mandatory = $false, ParameterSetName = "Remove")]
        [string]
        $VMFilesPath = "V:\VMs",

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

    try {
        if ($Force -eq $false) {
            if ($Type -eq "Remove") {
                Confirm-Question -Question "are you sure to remove all vms on this host?"
            }
            elseif ($Type -eq "ToSnapshot") {
                Confirm-Question -Question "are you sure to reset all vms on this host to the specified snapshot?"
            }
        }

        $VMs = Get-VM
        $VMs | ForEach-Object {
            Write-Verbose "found vm $($PSItem.Name)"
        }

        if ($Type -eq "Remove") {
            try {
                Write-Host "$($env:COMPUTERNAME): Turn of VMs"
                $VMs | Stop-VM -TurnOff -Force
            }
            catch {
                throw "could not turn off - $($PSItem.Exception.Message)"
            }
        
            try {
                Write-Host "$($env:COMPUTERNAME): Removing VMs"
                $VMs | Remove-VM -Force
            }
            catch {
                throw "could not remove - $($PSItem.Exception.Message)"
            }
        
            try {
                Write-Host "$($env:COMPUTERNAME): Removing VM Files"
                Remove-Item -Path $VMFilesPath  -Force -Recurse
            }
            catch {
                throw "could not remove files - $($PSItem.Exception.Message)"
            }
        }
        elseif ($Type -eq "ToSnapshot") {
            $VMs | ForEach-Object {
                try {
                    $Snapshot = Get-VMSnapshot -VM $PSItem -Name "*$($SnapShotNamePattern)*" -ErrorAction SilentlyContinue
                    if ($Snapshot.count -gt 1) {
                        throw "found more than 1 snapshot for $($PSItem.Name)"
                    }
                    elseif ($Snapshot.count -eq 0) {
                        Write-Warning "found no snapshot for $($PSItem.Name)"
                    }
                    else {
                        Write-Host "$($env:COMPUTERNAME): apply snapshot '$($Snapshot.Name)' to vm $($PSItem.Name)"
                        Restore-VMCheckpoint -VMSnapshot $Snapshot -Confirm:$False
                    }
                }
                catch {
                    throw "could not apply snapshot - $($PSItem.Exception.Message)"
                }
            }
        }
    }
    catch {
        throw "could not reset vm deployment - $($PSItem.Exception.Message)"
    }    
}

function Import-CourseVM {
    <#
    .SYNOPSIS
        imports vms from specified folder
 
    .DESCRIPTION
        imports vms from specified folder
 
    .PARAMETER VMFolderName
        Name of the folder under $($env:SystemDrive)\ClusterStorage\VMs where the vms are located
 
    .PARAMETER ShowOnly
        only display changes, but dont make the changes
 
    .EXAMPLE
        Import-CourseVM -VMFolderName PKI -ShowOnly
        shows only the possible vms to import from folder pki
 
    .EXAMPLE
        Import-CourseVM -VMFolderName PKI
        import vms of folder pki
 
    .NOTES
        - the vm files must be in the course folder under the $($env:SystemDrive)\ClusterStorage\VMs
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = "Default", Position = 2)]
        [switch]
        $ShowOnly
    )

    dynamicParam {
        # Set the dynamic parameters' name
        $ParameterName = 'VMFolderName'
        
        # Create and set the parameters' attributes
        $ParameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute
        $ParameterAttribute.Mandatory = $true
        $ParameterAttribute.Position = 1
        $ParameterAttribute.ParameterSetName = "Default"

        # Create the collection of attributes
        $AttributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]

        # Add the attributes to the attributes collection
        $AttributeCollection.Add($ParameterAttribute)

        # Generate and set the ValidateSet
        $arrSet = (Get-ChildItem -Path "$($env:SystemDrive)\ClusterStorage\VMs" | Where-Object -Property Name -NotLike "_*").Name
        $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)

        # Add the ValidateSet to the attributes collection
        $AttributeCollection.Add($ValidateSetAttribute)

        # Create and return the dynamic parameter
        $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)

        # Create the dictionary
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)

        return $RuntimeParameterDictionary
    }

    begin {
        # static variabels
        $VMFolderName = $PsBoundParameters[$ParameterName]
        $ErrorActionPreference = 'Stop'
        $ConsoleHighlightColor = "Green"

        $VMFolderPath = "$($env:SystemDrive)\ClusterStorage\VMs\$($VMFolderName)"
    }
    process {
        try {
            $VMConfigs = Get-ChildItem -Path $VMFolderPath -Recurse -Filter "Virtual Machines"
            $VMNames = $VMConfigs.FullName.Replace("Virtual Machines", "").Replace($VMFolderPath, "").Replace("\", "") | Sort-Object
            $MappingObject = $VMNames | ForEach-Object {
                return [PSCustomObject]@{
                    Name         = $PSItem
                    VMConfigPath = $VMConfigs.FullName -match $PSItem
                }
            }
        }
        catch {
            throw "could not fetch vm config files - $($PSItem.Exception.Message)"
        }
        
        Write-Host "found the following vm config files" -ForegroundColor $ConsoleHighlightColor
        $MappingObject | Format-Table -AutoSize

        if (-not $ShowOnly) {
            Confirm-Question -Question "do you want to start the import for those vms?"
            foreach ($Object in $MappingObject) {
                try {
                    $VMConfilePath = Get-ChildItem -Path $Object.VMConfigPath -Recurse -Filter "*.vmcx"
                    Write-Host "importing vm $($Object.Name)"
                    Write-Verbose "importing vm $($Object.Name), config file at $($VMConfilePath.FullName)"
                    Import-VM -Path $VMConfilePath.FullName
                }
                catch {
                    throw "could not import vm $($Object.Name), config file at $($VMConfilePath.FullName)"
                }
            }
        }
    }
}