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
 
        .Parameter VDiskPhysicalDiskRedundancy
        how many disks should be redundant
 
        .PARAMETER LogFileName
        name of the log file
 
        .PARAMETER LogFileFolderPath
        path of the folder where to put the log file
 
        .Example
        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,

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

        [Parameter(Mandatory = $false)]
        [string]
        $LogFileFolderPath = ""
    )

    $ErrorActionPreference = 'Stop'
    $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath

    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
                }
                Write-ToLogOrConsole @LogParam -Severity Info -Message "selected disks $($SelectedDisks.FriendlyName)"

                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) {
                    Write-ToLogOrConsole @LogParam -Severity Info -Message "creating volume $($VMVolumeName)"
                    Initialize-Disk -Number $SelectedDisks.DeviceId -PartitionStyle GPT | Out-Null
                    New-Volume -DiskNumber $SelectedDisks.DeviceId -FriendlyName $VMVolumeName -FileSystem ReFS -DriveLetter $VMDriveLetter | Out-Null
                }
                else {
                    Write-ToLogOrConsole @LogParam -Severity Info -Message "creating storage pool $($StoragePoolName)"
                    $StorageSubSystemFriendlyName = (Get-StorageSubSystem -FriendlyName "*Windows*").FriendlyName
                    New-StoragePool -StorageSubSystemFriendlyName $StorageSubSystemFriendlyName -FriendlyName $StoragePoolName -PhysicalDisks $SelectedDisks | Out-Null

                    if ($null -eq (Get-VirtualDisk -FriendlyName $VirtualDiskName -ErrorAction SilentlyContinue)) {
                        Write-ToLogOrConsole @LogParam -Severity Info -Message "create virtual disk $($VirtualDiskName) on $($StoragePoolName)"
                        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
                        }

                        Write-ToLogOrConsole @LogParam -Severity Info -Message "creating volume $($VMVolumeName)"
                        Initialize-Disk -FriendlyName $VirtualDiskName -PartitionStyle GPT | Out-Null
                        $VDiskNumber = (Get-Disk -FriendlyName $VirtualDiskName).Number
                        New-Volume -DiskNumber $VDiskNumber -FriendlyName $VMVolumeName -FileSystem ReFS -DriveLetter $VMDriveLetter | Out-Null
                    }
                    else {
                        Write-ToLogOrConsole @LogParam -Severity Info -Message "virtual disk $($VirtualDiskName) already exists - skipping"
                    }
                }
            }
            else {
                Write-ToLogOrConsole @LogParam -Severity Info -Message "pool $($StoragePoolName) already exists - skipping"
            }
        }
        else {
            Write-ToLogOrConsole @LogParam -Severity Info -Message "volume $($VMDriveLetter) already exists - skipping"
        }
    }
    catch {
        $ErrorMessage = "error during creation of vm volume: $($PSItem.Exception.Message)"
        Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
        throw $ErrorMessage
    }
}

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
 
        .PARAMETER LogFileName
        name of the log file
 
        .PARAMETER LogFileFolderPath
        path of the folder where to put the log file
 
        .Example
        New-VMVSwitch -Name 'IC'
 
        .NOTES
        there must be at least one nic with status 'up'
    #>


    param (
        [Parameter(Mandatory = $false)]
        [string]
        $Name = "LAN",

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

        [Parameter(Mandatory = $false)]
        [string]
        $LogFileFolderPath = ""
    )

    $ErrorActionPreference = 'Stop'
    $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath
    
    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-ToLogOrConsole @LogParam -Severity Info -Message "create vswitch $($Name) with netadapter $($Selected_NIC.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-ToLogOrConsole @LogParam -Severity Info -Message "virtual vswitch $($Name) already exists - skipping"
        }
    }
    catch {
        $ErrorMessage = "error during creation of virtual switch: $($PSItem.Exception.Message)"
        Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
        throw $ErrorMessage
    }
}

function Register-VMInConfigMgr {
    <#
        .Description
        Registers the VM in ConfigMgr with its MAC address for required deployments
 
        .Parameter Name
        name of the vm and so the name of device object in configmgr
 
        .Parameter MacAddress
        macaddres of the nic
 
        .Parameter CM_CollectionName
        configmgr collection name with the required deployment
 
        .Parameter CM_Siteserver_FQDN
        full qualified domain name of the site server
 
        .Parameter CM_Credentials
        configmgr credentials
 
        .Parameter SecondsToWait
        seconds to wait after the registration process has finished
 
        .PARAMETER LogFileName
        name of the log file
 
        .PARAMETER LogFileFolderPath
        path of the folder where to put the log file
 
        .Example
        Register-VMInConfigMgr -Name $PSItem.Name -MacAddress (Get-VM -Name $PSItem.Name | Get-VMNetworkAdapter).MacAddress -CM_CollectionName $PSItem.CM_CollectionName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -LogFileName $LogFileName
 
        .NOTES
         
    #>


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

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

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

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

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

        [Parameter(Mandatory = $false)]
        [int]
        $SecondsToWait = (Get-Random -Minimum 70 -Maximum 100),

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

        [Parameter(Mandatory = $false)]
        [string]
        $LogFileFolderPath = ""
    )

    $ErrorActionPreference = 'Stop'
    $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath
    $CM_Collection_All_Systems_ID = "SMS00001"

    #region functions
    function Confirm-CMCollectionMembership {
        [CmdletBinding()]
        param (
            [Parameter(Mandatory = $true)]
            [string]
            $DeviceName,
            
            [Parameter(Mandatory = $true)]
            [string]
            $CollectionName,

            [Parameter(Mandatory = $true)]
            [System.Management.Automation.Runspaces.PSSession]
            $CMPS_Session,

            [Parameter(Mandatory = $false)]
            [string]
            $LogFileName = "",
    
            [Parameter(Mandatory = $false)]
            [string]
            $LogFileFolderPath = ""
        )
    
        $ErrorActionPreference = 'Stop'
        $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath

        try {
            $Device_Exists_In_Collection = $false
            $Counter = 0
            $MaxCounter = 40
            do {
                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($DeviceName) - checking collection memberships in $($CollectionName) ($("{0:d2}" -f $Counter)/$($MaxCounter))"
                $CurrentCollectionMembers = Invoke-Command -Session $CMPS_Session -ArgumentList $DeviceName, $CollectionName -ScriptBlock {
                    try {
                        return (Get-CMCollectionMember -CollectionName $args[1] -Name $args[0])
                    }
                    catch {
                        throw $PSItem.Exception.Message
                    }
                }
    
                if ($null -ne $CurrentCollectionMembers) {
                    $Device_Exists_In_Collection = $true
                }
                else {
                    Start-Sleep -Seconds 10; $Counter++
                }
                if ($Counter -eq 12 -or $Counter -eq 24 -or $Counter -eq 36) {
                    Write-ToLogOrConsole @LogParam -Severity Info -Message "$($DeviceName) - doing cm collection update on $($CollectionName) ($("{0:d2}" -f $Counter)/$($MaxCounter))"
                    Invoke-Command -Session $CMPS_Session -ArgumentList $CollectionName -ScriptBlock {
                        Start-CMCollectionUpdate -CollectionName $args[0]
                    }
                }
            } while ($Device_Exists_In_Collection -eq $false -and $Counter -lt $MaxCounter)
    
            if ($Counter -ge $MaxCounter) {
                throw "could not find in the collection $($CollectionName) after a while"
            }
            else {
                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($DeviceName) - device collection in $($CollectionName)"
            }
        }
        catch {
            $ErrorMessage = "$($Name) - error during registration of device infos in configmgr: $($PSItem.Exception.Message)"
            Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
            throw $ErrorMessage
        }
    }
    #endregion

    try {
        Confirm-VMPresence -Name $Name

        # PS Session
        try {
            Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - Connecting to ConfigMgr $($CM_Siteserver_FQDN)"
            $CMPS_Session = New-PSSession -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials
            Invoke-Command -Session $CMPS_Session -ArgumentList $CM_Collection_All_Systems_ID -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):\"

                # define functions
                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) {
                        Invoke-CMCollectionUpdate -CollectionId $Collection.CollectionID
                        Start-Sleep -Seconds 5
                    }
                }

                # configmgr vars
                $CM_Collection_All_Systems_ID = $args[0]
                $CM_Collection_All_Systems_Name = (Get-CMCollection -Id $CM_Collection_All_Systems_ID).Name
                $CM_SiteCode = (Get-CMSite).SiteCode

                $CM_Collection_All_Systems_Name | Out-Null # just for the script analysis stuff
                $CM_SiteCode | Out-Null # just for the script analysis stuff
            }
            $CM_Collection_All_Systems_Name = Invoke-Command -Session $CMPS_Session -ArgumentList $CM_Collection_All_Systems_ID -ScriptBlock {
                return $CM_Collection_All_Systems_Name
            }
        }
        catch {
            $ErrorMessage = "error ps session to $($CM_Siteserver_FQDN): $($PSItem.Exception.Message)"
            Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
            throw $ErrorMessage
        }

        # checking existing devices
        try {
            $FoundExistingDevice = Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $MacAddress -ScriptBlock {
                try {
                    $Temp_CMDevice = Get-CMDevice -Name $args[0]
                    if (($null -ne $Temp_CMDevice) -and ($Temp_CMDevice.MACAddress.ToString().Replace(":", "") -ne $args[1])) {
                        return $true
                    }
                }
                catch {
                    throw $PSItem.Exception.Message
                }
            }
            if ($FoundExistingDevice -eq $true) {
                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - removing existing computer info in configmgr - macaddress $($MacAddress), because the mac address is not correct"
                Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $MacAddress -ScriptBlock {
                    try {
                        $Temp_CMDevice = Get-CMDevice -Name $args[0]
                        if (($null -ne $Temp_CMDevice) -and ($Temp_CMDevice.MACAddress.ToString().Replace(":", "") -ne $args[1])) {
                            Remove-CMDevice -Name $args[0] -Force -Confirm:$false
                        }
                    }
                    catch {
                        throw $PSItem.Exception.Message
                    }
                }
            }
        }
        catch {
            $ErrorMessage = "error checking/removing device in configmgr - $($PSItem.Exception.Message)"
            Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
            throw $ErrorMessage
        }

        # check destination collection existance
        try {
            $CollectionExits = Invoke-Command -Session $CMPS_Session -ArgumentList $CM_CollectionName -ScriptBlock {
                try {
                    if ($null -eq (Get-CMCollection -Name $args[0])) {
                        return $false
                    }
                    else {
                        return $true
                    }
                }
                catch {
                    throw $PSItem.Exception.Message
                }
            }
            if ($CollectionExits -eq $false) {
                $ErrorMessage = "collection $($CM_CollectionName) does not existing"
                Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
                throw $ErrorMessage
            }            
            Start-Sleep -Seconds 10
        }
        catch {
            $PSItem.Exception.Message
        }

        # import device
        try {
            Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - creating computer info in configmgr - macaddress $($MacAddress)"
            Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $MacAddress, $CM_CollectionName -ScriptBlock {
                try {
                    $Temp_CMDevice = Get-CMDevice -Name $args[0]
                    if ($null -eq $Temp_CMDevice) {
                        Import-CMComputerInformation -CollectionName $args[2] -ComputerName $args[0] -MacAddress $args[1]
                    }
                }
                catch {
                    throw $PSItem.Exception.Message
                }
            }
        }
        catch {
            $ErrorMessage = "error importing device in configmgr - $($PSItem.Exception.Message)"
            Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
            throw $ErrorMessage
        }

        # add device to target collection
        try {
            Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - adding computer info to target collection $($CM_CollectionName)"
            # checking all system refreshinterval
            Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $CM_CollectionName -ScriptBlock {
                try {
                    Start-CMCollectionUpdate -CollectionName $CM_Collection_All_Systems_Name
                }
                catch {
                    throw $PSItem.Exception.Message
                }
            }
            Start-Sleep -Seconds 10
            
            # check collection membership all systems
            Confirm-CMCollectionMembership -DeviceName $Name -CollectionName $CM_Collection_All_Systems_Name -CMPS_Session $CMPS_Session -LogFileName $LogParam.LogFileName

            # create membership rule
            Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $CM_CollectionName -ScriptBlock {
                try {
                    Add-CMDeviceCollectionDirectMembershipRule -CollectionName $args[1] -ResourceID (Get-CMDevice -Name $args[0]).ResourceID
                }
                catch {
                    throw $PSItem.Exception.Message
                }
            }
            Start-Sleep -Seconds 5

            # check collection membership target
            Confirm-CMCollectionMembership -DeviceName $Name -CollectionName $CM_CollectionName -CMPS_Session $CMPS_Session -LogFileName $LogParam.LogFileName
        }
        catch {
            $ErrorMessage = "error adding device to target collection - $($PSItem.Exception.Message)"
            Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
            throw $ErrorMessage
        }

        # remove collections, so there is only the targeted
        try {
            $CollectionMembershipsToRemove = Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $CM_CollectionName -ScriptBlock {
                try {
                    $Temp_CMDevice = Get-CMDevice -Name $args[0]
                    $TargetCollection = Get-CMCollection -Name $args[1]
                    if ($null -ne $Temp_CMDevice) {
                        return 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
                    }
                }
                catch {
                    throw $PSItem.Exception.Message
                }
            }
            if ($null -ne $CollectionMembershipsToRemove) {
                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name): removing additional collection membership in $($CollectionMembershipsToRemove.CollectionID)"
                Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $CollectionMembershipsToRemove -ScriptBlock {
                    try {
                        $Temp_CMDevice = Get-CMDevice -Name $args[0]
                        if ($null -ne $Temp_CMDevice) {
                            $args[1] | ForEach-Object {
                                $MembershipRule = Get-CMCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID | Where-Object -Property RuleName -EQ $Temp_CMDevice.Name
                                if ($null -ne $MembershipRule) {
                                    Remove-CMDeviceCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID -ResourceId $MembershipRule.ResourceId -Confirm:$false -Force
                                }
                            }
                        }
                    }
                    catch {
                        throw $PSItem.Exception.Message
                    }
                }
                
            }
        }
        catch {
            $ErrorMessage = "error removing additional collections - $($PSItem.Exception.Message)"
            Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
            throw $ErrorMessage
        }

        Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - finished registration, now waiting $($SecondsToWait) seconds for the configmgr collection updates and give the old man some time"
        Start-Sleep -Seconds $SecondsToWait
    }
    catch {
        $ErrorMessage = "$($Name) - error during registration of device infos in configmgr: $($PSItem.Exception.Message)"
        Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
        throw $ErrorMessage
    }
    finally {
        Remove-PSSession -Session $CMPS_Session
    }
}

function Confirm-VMDeployment {
    <#
        .Description
        Checks the ConfigMgr database to see if the deployment of VM objects is complete.
 
        .Parameter Name
        Name of the VM
 
        .Parameter CM_CollectionName
        ConfigMgr Collection Name with the required Deployment
 
        .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
 
        .Parameter WaitForAutoPilotDevicesInSeconds
        defines the amount of seconds to wait for a device after the last autopilot ts step
 
        .Example
        Confirm-VMDeployment -Name "VM-01" -CM_CollectionName $CM_CollectionName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials
 
        .NOTES
        To detect Autopilot Task Sequences there need to be as Last Step "Autopilot: Remove unattend.xml from Panther"
    #>


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

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

        [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 = 250,

        [Parameter(Mandatory = $false)]
        [int]
        $WaitForCMDatabaseThresholdInSeconds = 700,

        [Parameter(Mandatory = $false)]
        [int]
        $WaitForAutoPilotDevicesInSeconds = 240,

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

        [Parameter(Mandatory = $false)]
        [string]
        $LogFileFolderPath = ""
    )

    $ErrorActionPreference = 'Stop'
    $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath
    $AutoPilot_StepName = "Autopilot: Remove unattend.xml from Panther"

    # PS Session
    try {
        Confirm-VMPresence -Name $Name
        $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)"
    }

    # set environment
    try {
        $ResourceID = Invoke-Command -Session $CMPS_Session -ArgumentList $Name -ScriptBlock {
            try {
                return (Get-CMDevice -Name $args[0] -Fast).ResourceID
            }
            catch {
                throw $PSItem.Exception.Message
            }
        }
        $DeploymentID = Invoke-Command -Session $CMPS_Session -ArgumentList $CM_CollectionName -ScriptBlock {
            try {
                return (Get-CMDeployment -CollectionName $args[0]).DeploymentID
            }
            catch {
                throw $PSItem.Exception.Message
            }
        }
        $TaskSequenceName = Invoke-Command -Session $CMPS_Session -ArgumentList $CM_CollectionName -ScriptBlock {
            try {
                return (Get-CMDeployment -CollectionName $args[0]).SoftwareName
            }
            catch {
                throw $PSItem.Exception.Message
            }
        }
        $DeploymentProperties = @{
            ResourceID             = $ResourceID
            DeploymentID           = $DeploymentID
            TaskSequenceName       = $TaskSequenceName
            AutopilotTaskSequence  = $false
            Started                = $false
            StartDate              = $null
            Duration               = $null
            PXEFlagSet             = $false
            SummarizationTriggered = $false
            PXEFlagCleared         = $false
            FoundDataInConfigMgrDB = $false
            Running                = $false
            Finished               = $false
        }

        # output currently used task sequence for vm
        Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - the task sequence $($DeploymentProperties.TaskSequenceName) is used for the os deployment"
    }
    catch {
        throw "could not set environment - $($PSItem.Exception.Message)"
    }

    # check status
    try {
        # check if deployment has started
        do {
            Write-Verbose "current DeploymentProperties"
            $DeploymentProperties.Keys | ForEach-Object {
                Write-Verbose "$($PSItem)_$($DeploymentProperties."$($PSitem)")"
            } 
            # get configmgr device pxe flag status
            # https://katystech.blog/configmgr/delving-into-the-last-pxe-advertisement-flag
            $PXEAdvertisementStatus = Invoke-Command -Session $CMPS_Session -ArgumentList $Name -ScriptBlock {
                $Device_PXE_Properties = Get-CimInstance -Namespace "ROOT\SMS\site_$((Get-CMSite).SiteCode)" -Query "SELECT NetbiosName,AdvertisementID,LastPXEAdvertisementTime FROM SMS_LastPXEAdvertisement WHERE NetbiosName = '$($args[0])'"
                if($Device_PXE_Properties.LastPXEAdvertisementTime -eq "" -or $null -eq $Device_PXE_Properties.LastPXEAdvertisementTime) { 
                    # "not booted --> PXE Flag not set"
                    $PXEFlagSet = $false
                }
                elseif($Device_PXE_Properties.LastPXEAdvertisementTime -ne "") { 
                    # "booted at $($Device_PXE_Properties.LastPXEAdvertisementTime)--> PXE Flag set"
                    $PXEFlagSet = $true 
                }
                return @{
                    PXEFlagSet = $PXEFlagSet
                    LastPXEAdvertisementTime = $Device_PXE_Properties.LastPXEAdvertisementTime
                }
            }
            $DeploymentProperties.PXEFlagSet = $PXEAdvertisementStatus.PXEFlagSet
            $DeploymentProperties.LastPXEAdvertisementTime = $PXEAdvertisementStatus.LastPXEAdvertisementTime

            Write-Verbose "$($Name): DeploymentProperties.PXEFlagSet_$($DeploymentProperties.PXEFlagSet) DeploymentProperties.LastPXEAdvertisementTime_$($DeploymentProperties.LastPXEAdvertisementTime)"
            if ($DeploymentProperties.PXEFlagSet -eq $true) {
                $DeploymentProperties.StartDate = Get-Date
                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - pxe flag set, deployment has started"
                $DeploymentProperties.Started = $true
            }
            else {
                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - pxe flag not set, deployment not started yet"
                Start-Sleep -Seconds 15
            }
        } while ($DeploymentProperties.Started -eq $false)

        # check status of deployment
        if ($DeploymentProperties.Started -eq $true) {
            $DeploymentProperties.Running = $true
            do {
                $StatusMessages = $null
                $DeploymentProperties.Duration = (Get-Date) - $DeploymentProperties.StartDate                
                
                Write-Verbose "$($Name): current DeploymentProperties"
                $DeploymentProperties.Keys | ForEach-Object {
                    Write-Verbose "$($Name): $($PSItem)_$($DeploymentProperties."$($PSitem)")"
                } 

                if ($DeploymentProperties.PXEFlagSet -eq $true) {
                    # get current status message deployment from configmgr db
                    $StatusMessages = Invoke-Command -Session $CMPS_Session -ArgumentList $DeploymentProperties.ResourceID, $DeploymentProperties.DeploymentID, $CM_Siteserver_FQDN -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 "$($args[2])\$($CM_SiteCode)" -Database "CM_$($CM_SiteCode)" -Query $Query | Sort-Object -Property Step -Descending)
                        }
                        catch {
                            throw "could not get data from db - $($PSItem.Exception.Message)"
                        }
                    }
                    Write-Verbose "$($Name): current StatusMessages $($StatusMessages)"

                    # output current status
                    try {
                        if ($null -eq $StatusMessages -or $StatusMessages -eq "") {
                            Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - waiting on data in configmgr database, $("{0}" -f [math]::round($DeploymentProperties.Duration.TotalSeconds,1)) seconds since start ($($PXEClearAfterInSeconds)/$($WaitForCMDatabaseThresholdInSeconds)), PXE Flag set on $($DeploymentProperties.LastPXEAdvertisementTime)"
                        }
                        else {
                            $DeploymentProperties.FoundDataInConfigMgrDB = $true
                            $StatusObject = @{
                                "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 = "no actionname, last was $($StatusMessages[1].ActionName)"
                            }
                            elseif ($StatusMessages[2].ActionName -ne "") {
                                $StatusObject.ActionName = "no actionname, last was $($StatusMessages[2].ActionName)"
                            }
                            elseif ($StatusMessages[3].ActionName -ne "") {
                                $StatusObject.ActionName = "no actionname, last was $($StatusMessages[3].ActionName)"
                            }
                            elseif ($StatusMessages[4].ActionName -ne "") {
                                $StatusObject.ActionName = "no actionname, last was $($StatusMessages[4].ActionName)"
                            }
                            elseif ($StatusMessages[5].ActionName -ne "") {
                                $StatusObject.ActionName = "no actionname, last was $($StatusMessages[5].ActionName)"
                            }
                            elseif ($StatusMessages[6].ActionName -ne "") {
                                $StatusObject.ActionName = "no actionname, last was $($StatusMessages[6].ActionName)"
                            }
                            else {
                                $StatusObject.ActionName = "no actionname, couldnt find it"
                            }
                            #endregion

                            Write-Verbose "$($Name): currentstatusmessage $($StatusObject.LastStatusMessageIDName)"

                            if ($StatusObject.ActionName -like "*$($AutoPilot_StepName)*" ) {
                                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - the task sequence contains the step $($AutoPilot_StepName) - continue with the corresponding steps"
                                $DeploymentProperties.AutopilotTaskSequence = $true
                                $DeploymentProperties.Running = $false
                            }
                            elseif ($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*"
                            ) {
                                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - finished deployment"
                                $DeploymentProperties.Running = $false
                            }
                            else {
                                if ($StatusObject.Step.ToString().Length -eq 1) { $StepNumber = "00$($StatusObject.Step)" }
                                elseif ($StatusObject.Step.ToString().Length -eq 2) { $StepNumber = "0$($StatusObject.Step)" }
                                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - step $($StepNumber) - $($StatusObject.ActionName): $($StatusObject.LastStatusMessageIDName)"
                            }
                        }
                    }
                    catch {
                        throw "could not generate output - $($PSItem.Exception.Message)"
                    }
                }
                else {
                    Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - pxe flag not set, deployment not started yet, $("{0}" -f [math]::round($DeploymentProperties.Duration.TotalSeconds,1)) seconds since start ($($PXEClearAfterInSeconds)/$($WaitForCMDatabaseThresholdInSeconds))"
                }

                #region trigger cm deployment summarization
                Write-Verbose "$($Name): DisableAutoClearPXE_$($DisableAutoClearPXE) DeploymentProperties.Duration.TotalSeconds_$($DeploymentProperties.Duration.TotalSeconds) PXEFlagCleared_$($DeploymentProperties.PXEFlagCleared) FoundDataInConfigMgrDB_$($DeploymentProperties.FoundDataInConfigMgrDB)"
                if (
                    $DisableAutoClearPXE -eq $false -and `
                    $DeploymentProperties.Duration.TotalSeconds -ge ($PXEClearAfterInSeconds * 1 / 2 ) -and `
                    $DeploymentProperties.Duration.TotalSeconds -lt $WaitForCMDatabaseThresholdInSeconds -and `
                    $DeploymentProperties.SummarizationTriggered -ne $true -and `
                    $DeploymentProperties.FoundDataInConfigMgrDB -eq $false
                ) {
                    Write-ToLogOrConsole @LogParam -Severity Warning -Message "$($Name) - triggering cm deployment summarization on collection $($CM_CollectionName)"
                    Invoke-Command -Session $CMPS_Session -ArgumentList $CM_CollectionName -ScriptBlock {
                        try {
                            Invoke-CMDeploymentSummarization -CollectionName $args[0]
                        }
                        catch {
                            throw "could not trigger summarization - $($PSItem.Exception.Message)"
                        }
                    }
                    $DeploymentProperties.SummarizationTriggered = $true
                    Start-Sleep -Seconds 15
                }
                #endregion

                #region pxe clear after a period of time no boot was detected
                Write-Verbose "$($Name): DisableAutoClearPXE_$($DisableAutoClearPXE) DeploymentProperties.Duration.TotalSeconds_$($DeploymentProperties.Duration.TotalSeconds) PXEFlagCleared_$($DeploymentProperties.PXEFlagCleared) FoundDataInConfigMgrDB_$($DeploymentProperties.FoundDataInConfigMgrDB)"
                if (
                    $DisableAutoClearPXE -eq $false -and `
                    $DeploymentProperties.Duration.TotalSeconds -ge $PXEClearAfterInSeconds -and `
                    $DeploymentProperties.Duration.TotalSeconds -lt $WaitForCMDatabaseThresholdInSeconds -and `
                    $DeploymentProperties.PXEFlagCleared -ne $true -and `
                    $DeploymentProperties.FoundDataInConfigMgrDB -eq $false
                ) {
                    Write-ToLogOrConsole @LogParam -Severity Warning -Message "$($Name) - 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 $Name | Stop-VM -TurnOff -Force
                    Invoke-Command -Session $CMPS_Session -ArgumentList $Name -ScriptBlock {
                        try {
                            Get-CMDevice -Name $args[0] -Fast | Clear-CMPxeDeployment
                        }
                        catch {
                            throw "could not clear pxe flag - $($PSItem.Exception.Message)"
                        }
                    }
                    $DeploymentProperties.PXEFlagCleared = $true
                    Start-Sleep -Seconds 7
                    Start-VM -Name $Name
                }
                #endregion

                # stops after timeout
                if ($DeploymentProperties.Duration.TotalSeconds -ge $WaitForCMDatabaseThresholdInSeconds -and $DeploymentProperties.FoundDataInConfigMgrDB -eq $false) {
                    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

                # check if deployment is finished
                if ($DeploymentProperties.Running -eq $false) {
                    $DeploymentProperties.Finished = $true
                }
                else {
                    Start-Sleep -Seconds 15
                }
            } while ($DeploymentProperties.Finished -eq $false -and $DeploymentProperties.Duration.TotalMinutes -le $TimeoutInMinutes)
        }

        if ($DeploymentProperties.AutopilotTaskSequence -eq $true) {
            Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - the task sequence contains the step $($AutoPilot_StepName) - waiting for $($WaitForAutoPilotDevicesInSeconds)"
            Start-Sleep -Seconds $WaitForAutoPilotDevicesInSeconds
        }
        if ($DeploymentProperties.Duration.TotalMinutes -ge $TimeoutInMinutes) {
            throw "deployment not finished after $($TimeoutInMinutes) mins, check the logs in configmgr or inside the vms"
        }
    }
    catch {
        $ErrorMessage = "$($Name) - error checking status - $($PSItem.Exception.Message)"
        Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
        throw $ErrorMessage
    }
    finally {
        Remove-PSSession -Session $CMPS_Session
    }
}

function New-VirtualMaschine {
    <#
        .Description
        creates a hyperv vm
 
        .Parameter Name
        name of of the virtual machine
 
        .Parameter Path
        file path of the virtual machine
         
        .Parameter vSwitchName
        virtual switch name
         
        .Parameter CPUCount
        amount of virutal cores
         
        .Parameter RAM
        amount of ram
         
        .Parameter DiskSize
        amount of os disk size
         
        .Parameter DynamicRAMEnabled
        should the vm use dynamic ram
         
        .Parameter vTPMEnabled
        should there be a virtual tpm
         
        .Parameter AutoStartEnabled
        shoudl the vm automatically start
 
        .Example
        New-VirtualMaschine -Name "VM01" -Path "V:\VMs" -vSwitchName "vSwitch01"
 
        .Example
        New-VirtualMaschine -Name "VM01" -Path "V:\VMs" -vSwitchName "vSwitch01" -CPUCount 4 -RAM 16GB -DiskSize 120GB -DynamicRAMEnabled $false -vTPMEnabled $true -AutoStartEnabled $true
 
        .NOTES
         
    #>


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

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

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

        [Parameter(Mandatory = $false)]
        [int]
        $CPUCount = 2,

        [Parameter(Mandatory = $false)]
        [Int64]
        $RAM = 4gb,

        [Parameter(Mandatory = $false)]
        [Int64]
        $DiskSize = 80GB,

        [Parameter(Mandatory = $false)]
        [bool]
        $DynamicRAMEnabled = $false,

        [Parameter(Mandatory = $false)]
        [bool]
        $vTPMEnabled = $false,

        [Parameter(Mandatory = $false)]
        [bool]
        $AutoStartEnabled = $false,

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

        [Parameter(Mandatory = $false)]
        [string]
        $LogFileFolderPath = ""
    )

    $ErrorActionPreference = 'Stop'
    $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath

    # solve possible input problems
    if ($Path[-1] -eq "\") {
        $Path = $Path.Substring(0, $Path.Length - 1)
    }
    
    try {
        $VMVHDXPath = ($Path + "\" + $Name + "\Virtual Hard Disks\" + $Name + ".vhdx")
        if (Test-Path -Path $VMVHDXPath) {
            throw "vhdx for $($Name) already exists, please remove it"
        }
        try {
            Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - creating vdhx $($VMVHDXPath)"
            New-VHD -Path $VMVHDXPath -SizeBytes $DiskSize -Dynamic | Out-Null
            Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - creating vm"
            New-VM -Name $Name -MemoryStartupBytes $RAM -Path $Path -Generation 2 -VHDPath $VMVHDXPath -BootDevice NetworkAdapter -SwitchName $vSwitchName | Out-Null
        }
        catch {
            $ErrorMessage = "error during creation of vhdx or vm - $($PSItem.Exception.Message)"
            Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
            throw $ErrorMessage
        }

        # configure vm additional settings
        try {
            Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - set vm cpu count to $($CPUCount)"
            Set-VMProcessor -VMName $Name -Count $CPUCount
            if ($DynamicRAMEnabled -eq $false) {
                if ((Get-VM -Name $Name).DynamicMemoryEnabled) {
                    Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - set vm static memory"
                    Set-VM -Name $Name -StaticMemory
                }
            }
            if ($vTPMEnabled -eq $true) {
                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - enable vm vTPM"
                Set-VMKeyProtector -VMName $Name -NewLocalKeyProtector
                Enable-VMTPM -VMName $Name
            }
            if ($AutoStartEnabled -eq $true) {
                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - enable vm automatic start with 10 seconds delay"
                Set-VM -AutomaticStartAction Start -VMName $Name -AutomaticStartDelay 10 
            }
            else {
                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - disable vm automatic start"
                Set-VM -AutomaticStartAction Nothing -VMName $Name
            }
            Set-VM -AutomaticStopAction ShutDown -VMName $Name
            Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - enable vm integration services"
            Get-VMIntegrationService -VMName $Name | Where-Object -Property Enabled -EQ $false | Enable-VMIntegrationService

            Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - configuring checkpoint settings"
            Set-VM -VMName $Name -AutomaticCheckpointsEnabled $false -CheckpointType Production
        }
        catch {
            $ErrorMessage = "error while setting properties - $($PSItem.Exception.Message)"
            Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
            throw $ErrorMessage
        }

        # configuring mac address
        Start-VM -Name $Name
        Start-Sleep -Seconds 1
        Stop-VM -Name $Name -Force -TurnOff
        Start-Sleep -Seconds 3
        $MacAddress = (Get-VM -Name $Name | Get-VMNetworkAdapter).MacAddress
        Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - set vm static mac address"
        Set-VMNetworkAdapter -VMName $Name -StaticMacAddress $MacAddress

        # verify vm was created
        Confirm-VMPresence -Name $Name
        Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - vm creation complete"
    }
    catch {
        $ErrorMessage = "$($Name) - error during creation - $($PSItem.Exception.Message)"
        Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
        throw $ErrorMessage
    }
}

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
        Test-VMConnection -VMId (Get-VM -Name VDC01).Id -LocalAdminCreds $VM_Credentials
 
        .NOTES
         
    #>


    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Guid]
        $VMId,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [PSCredential]
        $LocalAdminCreds 
    )

    $VM = Get-VM -Id $VMId
    try {
        Write-Verbose "------"
        if ($VM.State -eq "Off") {
            Write-Verbose "------"
            Write-Verbose "$($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-Verbose "$($VM.Name): heartbeat IC connected"
        }
        do {
            $WaitForMinitues = 5
            $TimeElapsed = $(Get-Date) - $StartTime
            Write-Verbose "$($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)
        return $PSReady
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Confirm-VMPresence {
    <#
        .Description
        checks if the vm is registered with the local hypervisor
 
        .Parameter Name
        name of the vm
 
        .Example
        Confirm-VMPresence -Name "VM-01"
 
        .NOTES
         
    #>


    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    try {
        if ($null -eq (Get-VM -Name $Name)) {
            throw "$($Name) could not be found"
        }
        else {
            Write-Verbose "found vm $($Name) on $($env:COMPUTERNAME)"
        }
    }
    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
        Add-VMDisk -VMName $VM.Name -VHDXPath ($VM.ConfigurationLocation + "\Virtual Hard Disks\" + $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-Verbose "$($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 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
        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,

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

        [Parameter(Mandatory = $false)]
        [string]
        $LogFileFolderPath = ""
    )

    $ErrorActionPreference = 'Stop'
    $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath

    try {
        Confirm-VMState -VMObject (Get-VM -Name $VMName) -VMCredential $VMCredential
        Write-ToLogOrConsole @LogParam -Severity Info -Message "joining vm $($VMName) to domain $($DomainName)"
        Invoke-Command -VMName $VMName -Credential $VMCredential -ArgumentList $DomainName, $OUPath, $DomainCredential, $NoReboot -ScriptBlock {
            try {
                Confirm-DomainConnectivity -DomainName $args[0]
                Start-Sleep -Seconds 1
                if ($args[3] -eq $true) {
                    if ($null -ne $args[1]) {
                        Add-Computer -Credential $args[2] -DomainName $args[0] -OUPath $args[1] -WarningAction SilentlyContinue
                    }
                    else {
                        Add-Computer -Credential $args[2] -DomainName $args[0] -WarningAction SilentlyContinue
                    }
                }
                else {
                    if ($null -ne $args[1]) {
                        Add-Computer -Credential $args[2] -DomainName $args[0] -OUPath $args[1] -Restart -WarningAction SilentlyContinue
                    }
                    else {
                        Add-Computer -Credential $args[2] -DomainName $args[0] -Restart -WarningAction SilentlyContinue
                    }
                }
            }
            catch {
                throw $PSItem.Exception.Message
            }
        }

        if ($NoReboot -eq $true) {
            Write-ToLogOrConsole @LogParam -Severity Info -Message "domainjoin of vm $($VMName) successfull - reboot required"
        }
        else {
            Write-ToLogOrConsole @LogParam -Severity Info -Message "domainjoin of vm $($VMName) successfull - vm will do reboot"
        }        
    }
    catch {
        $ErrorMessage = "error joining vm $($VMName) to domain $($DomainName) - $($PSItem.Exception.Message)"
        Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
        throw $ErrorMessage
    }
}

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 IPv4Address
        ip address that should be assigned
 
        .Parameter IPv4NetPrefix
        subnet prefix, aka 24 or 16
 
        .Parameter IPv4Gateway
        gateway of the subnet
 
        .Parameter IPv4DNSAddresses
        dns server ip addresses
 
        .Example
        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]
        $IPv4Address,

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

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

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

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

        [Parameter(Mandatory = $false)]
        [string]
        $LogFileFolderPath = ""
    )

    $ErrorActionPreference = 'Stop'
    $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath
    $funcDef_SetInterface = ${function:Set-Interface}.ToString()
    $VMNetworkInterface = Invoke-Command -VMName $VMName -Credential $VMCredential -ScriptBlock {
        try {
            $InterfaceObject = (Get-NetAdapter)[0]
            return $InterfaceObject
        }
        catch {
            throw "error getting nic interface - $($PSItem.Exception.Message)"
        }
    }

    Write-ToLogOrConsole @LogParam -Severity Info -Message "$($VMName) - configuring network interface with index $($VMNetworkInterface.InterfaceIndex)"
    Invoke-Command -VMName $VMName -Credential $VMCredential -ArgumentList $VMNetworkInterface, $IPv4Address, $IPv4NetPrefix, $IPv4Gateway, $IPv4DNSAddresses -ScriptBlock {
        try {
            ${function:Set-Interface} = $using:funcDef_SetInterface
            Set-Interface -InterfaceObject $args[0] -IPAddress $args[1] -NetPrefix $args[2] -DefaultGateway $args[3] -DNSAddresses $args[4]
        }
        catch {
            throw "error setting ip interface - $($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
        Confirm-VMState -VMObject $VM -VMCredential $VMCred
 
        .NOTES
 
    #>


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

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

    try {
        if ($VMObject.State -ne "Running") {
            Write-Verbose "starting $($VMObject.Name) because the vm was stopped"
            Start-VM -VM $VMObject
            Start-Sleep -Seconds 10
        }
        Write-Verbose "verify the connection to $($VMObject.Name)"
        if (Test-VMConnection -VMId $VMObject.Id -LocalAdminCreds $VMCredential) {
            Write-Verbose "connected to $($VMObject.Name) successful - continue"
        }
        else {
            throw "error while connecting to $($VMObject.Name) with ps direct - $($PSItem.Exception.Message)"
        }    
    }
    catch {
        throw "$($PSItem.Exception.Message)"
    }
}

function Confirm-HyperV {
    <#
        .Description
        this function throws errors, when hyper-v is not installed
 
        .Example
        Confirm-HyperV
 
        .NOTES
         
    #>


    try {
        $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"
            }
        }
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Set-VMInstallSnapshot {
    <#
        .Description
        this creates a snapshot
 
        .Parameter VMName
        name of vm
 
        .Parameter SnapshotName
        name of snapshot
 
        .Parameter VMCredential
        credentials for vm
 
        .Example
        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,

        [Parameter(Mandatory = $false)]
        [bool]
        $AutopilotDevice = $false
    )
    
    try {
        if ($AutopilotDevice -eq $false) {
            Confirm-VMState -VMObject (Get-VM -Name $VMName) -VMCredential $VMCredential
            Stop-VM -Name $VMName -Force
        }
        Checkpoint-VM -Name $VMName -SnapshotName $SnapshotName
        if ($AutopilotDevice -eq $false) {
            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
        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-Verbose "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 SwitchName
        name of the virtual switch that should be created
 
        .Parameter TrustedHostsValue
        value for trustedhosts to add, needed for configmgr things
 
        .Parameter VM_Drive_Letter
        letter for the vm volume, which will be created
 
        .PARAMETER LogFileName
        name of the log file
 
        .PARAMETER LogFileFolderPath
        path of the folder where to put the log file
 
        .Example
        New-VMHost -SwitchName $Course_Shortcut -TrustedHostsValue "$($CM_Siteserver_NetBIOS),$($CM_Siteserver_FQDN)"
 
        .NOTES
         
    #>


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

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

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

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

        [Parameter(Mandatory = $false)]
        [string]
        $LogFileFolderPath = ""
    )

    $ErrorActionPreference = 'Stop'
    $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath

    try {
        # first step - winrm for configmgr
        Write-Progress -Activity "New-VMHost" -CurrentOperation "configuring trusted hosts" -Status "running" -PercentComplete 0 

        Write-ToLogOrConsole @LogParam -Severity Info -Message "configuring winrm to allow connections with configmgr"
        Start-Service -Name "WinRM"
        Set-Item -Path "WSMan:\localhost\Client\TrustedHosts" -Value $TrustedHostsValue -Force -Concatenate
        Start-Sleep -Milliseconds 0.4 # just for the progress

        Write-ToLogOrConsole @LogParam -Severity Info -Message "setting hyper-v environment"
        # second step - prepare host for vms
        Write-Progress -Activity "New-VMHost" -CurrentOperation "creating volume" -Status "running" -PercentComplete 25
        New-VMVolume -VMDriveLetter $VM_Drive_Letter @LogParam
        Start-Sleep -Milliseconds 0.4 # just for the progress

        # third step - virtual switch
        Write-Progress -Activity "New-VMHost" -CurrentOperation "creating switch" -Status "running" -PercentComplete 50
        New-VMVSwitch -Name $SwitchName @LogParam
        Start-Sleep -Seconds 0.4 # just for the progress

        # fourth step - hyperv config
        Write-Progress -Activity "New-VMHost" -CurrentOperation "configuring hyperv" -Status "running" -PercentComplete 75
        $VM_DefaultPath = "$($VM_Drive_Letter):\VMs"
        Set-VMHost -EnableEnhancedSessionMode $True -VirtualHardDiskPath $VM_DefaultPath -VirtualMachinePath $VM_DefaultPath
        Start-Sleep -Milliseconds 0.4 # just for the progress
        Write-Progress -Activity "New-VMHost" -CurrentOperation "configuring hyperv" -Status "finished" -PercentComplete 100 -Completed
    }
    catch {
        $ErrorMessage = "host preparations failed - $($PSItem.Exception.Message)"
        Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
        throw $ErrorMessage
    }
}

function Install-VMs {
    <#
        .Description
        installs vms on localhost and configures a snapshot
 
        .Parameter VM_Config
        Object that may contains multiple descriptive objects for deployment from a VM. For Example:
        $VM_Config = @()
        $VM_01 = [PSCustomObject]@{
            Name = "$($Course_Shortcut)-VWIN11-$($Participant_Number)1"
            Path = $VMPath
            vSwitchName = $SwitchName
            CPUCount = $CPUCount
            RAM = $RAM
            DiskSize = $DynamicDiskSize
            DynamicRAMEnabled = $false
            vTPMEnabled = $true
            AutoStartEnabled = $true
            CM_CollectionName = $CM_Collection_W11_Autopilot
            Credentials = $VM_Credentials
            AutopilotDevice = $true
        }
        $VM_Config = $VM_Config + $VM_01
 
        .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
        Install-VMs -VM_Config $VM_Config -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -LogFileName "Install-VMs_$(Get-Date -Format "dd-MM-yyyy_hh.mm.ss").log"
 
        .NOTES
        VM need to have a static mac address and only one nic
 
        The Parameter VM_Config accepts the following Parameters for each object, based on New-VirtualMachine and Register-VMInConfigMgr:
        Name, Path, vSwitchName, CPUCount, RAM, DiskSize, DynamicRAMEnabled, vTPMEnabled, AutoStartEnabled, Credentials, AutopilotDevice, LogFileName, CM_CollectionName
    #>


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

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

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

        [Parameter(Mandatory = $false)]
        [int]
        $SecondsToWaitBeforeCreatingSnapshots = 120,
     
        [Parameter(Mandatory = $false)]
        [string]
        $SnapShotNameSuffix = "$(Get-Date -format "dd-MM-yyyy_hh:mm:ss") - initial deployment",

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

        [Parameter(Mandatory = $false)]
        [string]
        $LogFileFolderPath = ""
    )

    $ErrorActionPreference = 'Stop'
    $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath

    # checks
    if ($PSVersionTable.PSVersion -lt [version]"7.2.0") {
        throw "you have to use powershell 7.2.0 or higher"
    }

    # checking if the vm exists
    $VM_Config | ForEach-Object {
        if ($null -ne (Get-VM | Where-Object -Property Name -like $PSitem.Name)) {
            $ErrorMessage = "vm $($PSItem.Name) already exists, stopping"
            Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
            throw $ErrorMessage
        }
    }

    # deployment
    try {
        # stuff for write progress
        $origin = @{}
        $VM_Config | Foreach-Object {
            $origin.($PSItem.Name) = @{}
        }
        $sync = [System.Collections.Hashtable]::Synchronized($origin)

        # create vm deployment jobs
        $BaseActivityName = "Install-VMs"
        Write-Progress -Id 0 -Activity $BaseActivityName -Status "Processing" -PercentComplete 0
        $Jobs = $VM_Config | Sort-Object -Property Name | Foreach-Object -ThrottleLimit $VM_Config.count -AsJob -Parallel {
            try {
                $ErrorActionPreference = 'Stop'
                Import-Module -Name "NTS.Tools"
                $StepCountPerVM = 6

                # define vars from parent session
                $LogParam = $using:LogParam
                $CM_Siteserver_FQDN = $using:CM_Siteserver_FQDN
                $CM_Credentials = $using:CM_Credentials

                $syncCopy = $using:sync
                $process = $syncCopy.$($PSItem.Name)
                $process.ParentId = 0
                $process.Id = $($using:VM_Config).IndexOf($PSItem) + 1
                $process.Activity = "VM $($PSItem.Name)"
                $process.Status = "starting"
                $process.PercentComplete = 1
    
                # Fake workload start up that takes x amount of time to complete
                Start-Sleep -Milliseconds (3..10 | Get-Random | Foreach-Object { $PSItem * 100 })
    
                #region do stuff
                # 1 step
                $process.Status = "creating vm"
                New-VirtualMaschine -Name $PSItem.Name -Path $PSItem.Path -vSwitchName $PSItem.vSwitchName -CPUCount $PSItem.CPUCount -RAM $PSItem.RAM -DiskSize $PSItem.DiskSize -DynamicRAMEnabled $PSItem.DynamicRAMEnabled -vTPMEnabled $PSItem.vTPMEnabled -AutoStartEnabled $PSItem.AutoStartEnabled -LogFileName $LogParam.LogFileName
                $process.PercentComplete = [Math]::Round(1/$StepCountPerVM,2)*100

                # 2 step
                $process.Status = "registering vm"
                Register-VMInConfigMgr -Name $PSItem.Name -MacAddress (Get-VM -Name $PSItem.Name | Get-VMNetworkAdapter).MacAddress -CM_CollectionName $PSItem.CM_CollectionName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -LogFileName $LogParam.LogFileName
                $process.PercentComplete = [Math]::Round(2/$StepCountPerVM,2)*100

                # 3 step
                $process.Status = "starting deployment"
                try {
                    $DelayStartsInSeconds = (Get-Random -Minimum 3 -Maximum 5)
                    Write-Host "$($PSItem): $($LogParam)"
                    Write-ToLogOrConsole @LogParam -Severity Info -Message "$($PSItem.Name) - starting for pxe deployment, using delay of $($DelayStartsInSeconds)"
                    Start-Sleep -Seconds $DelayStartsInSeconds
                    Start-VM -VMName $PSItem.Name
                }
                catch {
                    throw "error while starting - $($PSItem.Exception.Message)"
                }
                $process.PercentComplete = [Math]::Round(3/$StepCountPerVM,2)*100

                # 4 step
                $process.Status = "verifying deployment"
                Confirm-VMDeployment -Name $PSItem.Name -CM_CollectionName $PSItem.CM_CollectionName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -VM_Credentials $PSItem.Credentials -LogFileName $LogParam.LogFileName
                $process.PercentComplete = [Math]::Round(4/$StepCountPerVM,2)*100

                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($PSItem.Name) - now waiting for $($using:SecondsToWaitBeforeCreatingSnapshots) seconds"
                Start-Sleep -Seconds $using:SecondsToWaitBeforeCreatingSnapshots

                # 5 step
                $process.Status = "creating snapshot"
                $SnapShotName = "$($PSItem.Name) - $($using:SnapShotNameSuffix)"
                Write-ToLogOrConsole @LogParam -Severity Info -Message "$($PSItem.Name) - creating snapshot $($SnapShotName)"
                Set-VMInstallSnapshot -VMName $PSItem.Name -SnapshotName $SnapShotName -VMCredential $PSItem.Credentials -AutopilotDevice $PSItem.AutopilotDevice
                $process.PercentComplete = [Math]::Round(5/$StepCountPerVM,2)*100

                # 6 step
                $process.Status = "doing cleanup"
                Remove-VMConfigMgrDeployment -Name $PSItem.Name -CM_CollectionName $PSItem.CM_CollectionName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -LogFileName $LogParam.LogFileName
                $process.PercentComplete = [Math]::Round(6/$StepCountPerVM,2)*100

                # Mark process as completed
                $process.Status = "finished"
                $process.Completed = $true
                #endregion
            }
            catch {
                $process.Status = "failed"
                $process.PercentComplete = 100
                $process.Completed = $true
                throw $PSItem.Exception.Message
            }
        }

        # verifiy jobs and create processing output
        while ($Jobs.State -eq 'Running') {
            try {
                # get percentage of completed
                $PercentageCompleted = $($sync.Values.PercentComplete | Measure-Object -Sum).Sum / $VM_Config.count
                if ($PercentageCompleted -lt 1) { $PercentageCompleted = 1 }
                $sync.Keys | Foreach-Object {
                    # If key is not defined, ignore
                    if (![string]::IsNullOrEmpty($sync.$PSItem.keys)) {
                        # Create parameter hashtable to splat
                        $param = $sync.$PSItem

                        if ($param.Status -eq "failed") {
                            throw "job for $($param.Activity) failed"
                        }

                        # Execute Write-Progress
                        Write-Progress -Id 0 -Activity $BaseActivityName -Status "running" -PercentComplete $PercentageCompleted
                        Write-Progress @param
                    }
                }

                # Wait to refresh to not overload gui
                Start-Sleep -Seconds 0.4
            }
            catch {
                $FailedJobs = $Jobs | Get-Job -IncludeChildJob | Where-Object -Property State -eq "Failed"
                if ($null -ne $FailedJobs) {
                    Write-Host "jobs with id $($FailedJobs.Id) failed, troubleshoot using the following command and viewing the log file" -ForegroundColor Red
                    Write-Host "(Get-Job -Id $($FailedJobs[0].Id) -IncludeChildJob).JobStateInfo" -ForegroundColor Red
                }
                throw $PSItem.Exception.Message
            }
        }
        Write-Progress -Id 0 -Activity $BaseActivityName -Status "Finished" -PercentComplete 100 -Completed
    }
    catch {
        Write-Verbose "stopping running jobs"
        $Jobs | Stop-Job -Confirm:$false
        $ErrorMessage = "error deploying vms - $($PSItem.Exception.Message)"
        Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
        throw $ErrorMessage
    }
}

function Remove-VMConfigMgrDeployment {
    <#
        .Description
        Removes the ConfigMgr Object related to the vms
 
        .Parameter Name
        Name of the VM
 
        .Parameter CM_CollectionName
        ConfigMgr Collection Name with the required Deployment
 
        .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
        Remove-VMConfigMgrDeployment -Name $PSItem.Name -CM_CollectionName $PSItem.CM_CollectionName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -LogFileName $LogFileName
 
        .NOTES
         
    #>


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

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

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

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

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

        [Parameter(Mandatory = $false)]
        [string]
        $LogFileFolderPath = ""
    )

    $ErrorActionPreference = 'Stop'
    $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath

    # PS Session
    try {
        Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - Connecting to ConfigMgr $($CM_Siteserver_FQDN)"
        $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 {
        $ErrorMessage = "error ps session to $($CM_Siteserver_FQDN): $($PSItem.Exception.Message)"
        Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
        throw $ErrorMessage
    }

    try {
        # remove collections membership rules
        Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - removing collection membership in $($CM_CollectionName) after the deployment"
        Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $CM_CollectionName -ScriptBlock {
            try {
                $CM_SiteCode = (Get-CMSite).SiteCode
                $Temp_CMDevice = Get-CMDevice -Name $args[0]
                $TargetCollection = Get-CMCollection -Name $args[1]
                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) {
                                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
        Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - removing computer info in configmgr after the deployment"
        Invoke-Command -Session $CMPS_Session -ArgumentList $Name -ScriptBlock {
            try {
                $Temp_CMDevice = Get-CMDevice -Name $args[0]
                if ($null -ne $Temp_CMDevice) {
                    Remove-CMDevice -Name $args[0] -Force -Confirm:$false
                }
            }
            catch {
                throw "error removing device - $($PSItem.Exception.Message)"
            }
        }
    }
    catch {
        $ErrorMessage = "$($Name) - $($PSItem.Exception.Message)"
        Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
        throw $ErrorMessage
    }
    finally {
        Remove-PSSession -Session $CMPS_Session
    }
}

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
        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",

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

        [Parameter(Mandatory = $false)]
        [string]
        $LogFileFolderPath = ""
    )

    $ErrorActionPreference = 'Stop'
    $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath

    try {
        $VM = Get-VM -Name $VMName
        $CurrentVMDiskCount = (Get-VHD -VMId $VM.Id).count
        if ($null -eq $VHDXPathXPath -or $VHDXPathXPath -eq "") {
            $VHDXPathXPath = "$($VM.ConfigurationLocation)\Virtual Hard Disks\$($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-ToLogOrConsole @LogParam -Severity Info -Message "adding disk to $($VM.Name)"
            Add-VMDisk -VMName $VM.Name -VHDXPath $VHDXPathXPath -VHDXSize $VolumeSize
            Invoke-Command -VMName $VM.Name -Credential $VMCredential -ScriptBlock {
                try {
                    Write-ToLogOrConsole @LogParam -Severity Info -Message "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-ToLogOrConsole @LogParam -Severity Info -Message "$($VM.Name): volume already exits"
        }
    }
    catch {
        $ErrorMessage = "could not format volume - $($PSItem.Exception.Message)"
        Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage
        throw $ErrorMessage
    }
}

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
 
        .EXAMPLE
        Connect-CourseVMToNetwork -VMFolderName PKI -CourseRoom '4OG(Oben)'
 
        .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 = $false)]
        [ValidateSet("Local", "Cluster")]
        [string]
        $HostType,

        [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
        )

        # get Cluster info
        if ($HostType -eq "Cluster") {
            $ComputerNames = (Get-ClusterNode).Name | ForEach-Object { $PSItem + "." + $env:USERDNSDOMAIN }
        }
        elseif ($HostType -eq "Local") {
            $ComputerNames = $env:COMPUTERNAME
        }

        # collect VM info
        try {
            $VMs = Get-VM -ComputerName $ComputerNames | Where-Object -Property Path -like "$($env:SystemDrive)\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

        # set settings
        if ($ShowOnly) {
            Write-Host "`nshowing change that would apply" -ForegroundColor $ConsoleHighlightColor
        }
        else {
            Confirm-Question -Question "do you want to proceed?"
        }

        foreach ($HostName in $ComputerNames) {
            $CurrentHostVMs = $VMs | Where-Object -Property Computername -eq $HostName.Split(".")[0]
            $CurrentHostVMs | ForEach-Object {
                $VMNetadapters = Get-VMNetworkAdapter -VM $PSItem
                if ($null -eq $VMNetadapters) {
                    throw "did not find any vm netadapters for $($PSitem.Name)"
                }
                else {
                    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
 
        .EXAMPLE
        Restore-CourseVMToLastSnapshot -VMFolderName 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 "$($env:SystemDrive)\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
        Reset-VMDeployment -Type Remove -Force
 
        .Example
        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?"
            }
        }

        $VMsToRemove = (Get-VM).Name | Where-Object -FilterScript { (Get-ChildItem -Path $VMFilesPath -ErrorAction SilentlyContinue).Name -contains $PSItem }
        $VMsToRemove | ForEach-Object {
            Write-Verbose "found vm $($PSItem.Name)"
        }

        if ($Type -eq "Remove") {
            try {
                Write-Host "Turn of VMs"
                $VMsToRemove | Stop-VM -TurnOff -Force -WarningAction SilentlyContinue
            }
            catch {
                throw "could not turn off - $($PSItem.Exception.Message)"
            }
        
            try {
                Write-Host "Removing VMs"
                $VMsToRemove | Remove-VM -Force
            }
            catch {
                throw "could not remove - $($PSItem.Exception.Message)"
            }
        
            try {
                Write-Host "Removing VM Files"
                Remove-Item -Path $VMFilesPath  -Force -Recurse
            }
            catch {
                throw "could not remove files - $($PSItem.Exception.Message)"
            }
        }
        elseif ($Type -eq "ToSnapshot") {
            $VMsToRemove | 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 "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
 
    .EXAMPLE
        Import-CourseVM -VMFolderName 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,

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

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

                if ($AddToClusterAsResource) {
                    try {
                        Write-Host "adding vm $($Object.Name) to cluster as role"
                        Add-ClusterVirtualMachineRole -VMName $Object.Name
                    }
                    catch {
                        throw "could not add vm $($Object.Name) to cluster - $($VMConfilePath.FullName)"
                    }
                }
            }
        }
    }
}

function New-CourseVM {
    <#
        .SYNOPSIS
        creates a new vm
 
        .DESCRIPTION
        creates a new vm in its course folder
 
        .PARAMETER VMFolderName
        Name of the folder under $($env:SystemDrive)\ClusterStorage\VMs where the vms are located
 
        .PARAMETER HostType
        localhost or cluster, which are all nodes in the cluster
 
        .PARAMETER VMName
        Name of vm
 
        .PARAMETER CPUCount
        count of vcores
 
        .PARAMETER RAM
        amount of ram as 4gb for example
 
        .PARAMETER OSDiskSize
        disk size for os as 120gb for example
 
        .PARAMETER VMSwitchName
        name of virtual switch
 
        .PARAMETER AutoStartEnabled
        should the autostart functionality be enabled
 
        .PARAMETER vTPMEnabled
        should a vtpm be added to the vm
 
        .EXAMPLE
        New-CourseVM -VMFolderName IC -VMName "IC-VWIN11-01" -HostType Cluster
 
        .NOTES
        - the folder must be in the course folder under the $($env:SystemDrive)\ClusterStorage\VMs
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("Local", "Cluster")]
        [string]
        $HostType,

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

        [Parameter(Mandatory = $false)]
        [int]
        $CPUCount = 4,

        [Parameter(Mandatory = $false)]
        [int64]
        $RAM = 4gb, 

        [Parameter(Mandatory = $false)]
        [int64]
        $OSDiskSize = 120GB,

        [Parameter(Mandatory = $false)]
        [string]
        $VMSwitchName = "SCHULUNG",

        [Parameter(Mandatory = $false)]
        [bool]
        $AutoStartEnabled = $false,

        [Parameter(Mandatory = $false)]
        [bool]
        $vTPMEnabled = $false 
    )

    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 {
        # static variabels
        $VMFolderName = $PsBoundParameters[$ParameterName]
        $ErrorActionPreference = 'Stop'

        # get Cluster info
        if ($HostType -eq "Cluster") {
            $ComputerNames = (Get-ClusterNode).Name | ForEach-Object { $PSItem + "." + $env:USERDNSDOMAIN }
        }
        elseif ($HostType -eq "Local") {
            $ComputerNames = $env:COMPUTERNAME
        }

        # collect VM info
        try {
            $VMs = Get-VM -ComputerName $ComputerNames | Where-Object -Property Path -like "$($env:SystemDrive)\ClusterStorage\VMs\$($VMFolderName)\*"
        }
        catch {
            throw "could not fetch vms - $($PSItem.Exception.Message)"
        }

        $ComputerNames | ForEach-Object {
            Write-Verbose "using physical server $($PSItem)"
        }

        $VMPath = "$($env:SystemDrive)\ClusterStorage\VMs\$($VMFolderName)\"
    }

    process {
        if ($VMs.name -contains $VMName) {
            throw "vm $($VMName) already exists"
        }
        else {
            $VMVHDXPath = ($VMPath + "\" + $VMName + "\Virtual Hard Disks\" + $VMName + ".vhdx")
            Write-Output "creating vm $($VMName) on $($env:COMPUTERNAME)"
            try {
                if (Test-Path -Path $VMVHDXPath) {
                    throw "vhdx for $($VMName) already exists, please remove it"
                }
    
                New-VHD -Path $VMVHDXPath -SizeBytes $OSDiskSize -Dynamic | Out-Null
                New-VM -Name $VMName -MemoryStartupBytes $RAM -Path $VMPath -Generation 2 -VHDPath $VMVHDXPath -BootDevice NetworkAdapter -SwitchName $VMSwitchName | Out-Null
            }
            catch {
                throw "error during creation of vhdx or vm $($VMName) - $($PSItem.Exception.Message)"
            }
            try {
                $CreatedVM = Get-VM -Name $VMName
                Set-VMProcessor -VMName $VMName -Count $CPUCount
                if ($CreatedVM.DynamicMemoryEnabled) {
                    Set-VM -Name $VMName -StaticMemory
                }
                if ($vTPMEnabled) {
                    Set-VMKeyProtector -VMName $VMName -NewLocalKeyProtector
                    Enable-VMTPM -VMName $VMName
                }
                if ($AutoStartEnabled) {
                    Set-VM -AutomaticStartAction Start -VMName $VMName -AutomaticStartDelay 10 
                }
                else {
                    Set-VM -AutomaticStartAction Nothing -VMName $VMName
                }
                Set-VM -AutomaticStopAction ShutDown -VMName $VMName
                Get-VMIntegrationService -VMName $VMName | Where-Object -Property Enabled -EQ $false | Enable-VMIntegrationService
            }
            catch {
                throw "error while setting properties $($VMName) - $($PSItem.Exception.Message)"
            }

            if ($HostType -eq "Cluster") {
                Write-Output "adding vm $($VMName) to cluster as role"
                Add-ClusterVirtualMachineRole -VMName $VMName | Out-Null
            }
        }
    }
}

function Update-CourseVM {
    <#
    .SYNOPSIS
        starts windows update process
 
    .DESCRIPTION
        starts windows update process
 
    .PARAMETER VMName
        Name of vm
 
    .PARAMETER VMCredential
        credentials for the vm
 
    .PARAMETER AutoReboot
        should the vm reboot after applying the updates
 
    .EXAMPLE
        Update-CourseVM -VMName "IC-VWIN11-01" -VMCredential $VMCredential -AutoReboot $true
        Updates the OS from the VM "IC-VWIN11-01" and restarts it afterwards
 
    .NOTES
         
    #>


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

        [Parameter(Mandatory = $false)]
        [bool]
        $AutoReboot = $false
    )

    begin {
        # load functions from host into vars
        $funcDef_InitializePowerShellEnviroment = ${function:Initialize-PowerShellEnviroment}.ToString()
        $funcDef_UpdateWindowsSystem = ${function:Update-WindowsSystem}.ToString()

        # static variabels
        $ErrorActionPreference = 'Stop'

        # collect VM info
        try {
            Write-Verbose "checking if vm $($VMName) exists"
            $VM = Get-VM -Name $VMName -ErrorAction SilentlyContinue
            if ($null -eq $VM) {
                throw "could not find vm"
            }

            $VMOS = Get-VMOperatingSystem -VMName $VMName
            if ($null -eq $VMOS -or $VMOS -eq "") {
                Write-Warning "could not determine vm os, skipping this vm $($VMName)"
            }
            elseif ($VMOS -notlike "*Windows*") {
                Write-Warning "vm $($VMName) has not windows, skipping"
            }
            elseif ($VMOS -like "*Windows*") {
                Write-Verbose "vm $($VMName) has windows"
            }
        }
        catch {
            throw "could not fetch vm info - $($PSItem.Exception.Message)"
        }
    }

    process {
        try {
            Write-Verbose "connecting to vm $($VMName) via ps direct"
            Invoke-Command -VMName $VMName -Credential $VMCredential -ScriptBlock {
                Write-Verbose "connected to vm $($using:VMName)"
                # load funtions from vars into vm session
                Write-Verbose "start loading functions"
                ${function:Initialize-PowerShellEnviroment} = $using:funcDef_InitializePowerShellEnviroment
                ${function:Update-WindowsSystem} = $using:funcDef_UpdateWindowsSystem

                # start update process
                try {
                    Initialize-PowerShellEnviroment
                    Update-WindowsSystem -AutoReboot $using:AutoReboot
                }
                catch {
                    throw "error updating vm - $($PSItem.Exception.Message)"
                }
            }
        }
        catch {
            throw "could not update vm $($VMName)"
        }
    }
}

function Update-CourseEnvironment {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [pscredential]
        $VMCredential,

        [Parameter(Mandatory = $false)]
        [bool]
        $AutoReboot = $false,

        [Parameter(Mandatory = $false)]
        [ValidateScript({ $PSItem -gt 0 -and $PSItem -le 10 })]
        [int]
        $ThrottleLimit = 3,

        [Parameter(Mandatory = $false)]
        [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 {
        Write-Warning "this function is still work in progress, use with care"

        # static variabels
        $VMFolderName = $PsBoundParameters[$ParameterName]
        $funcDef_InitializePowerShellEnviroment = ${function:Initialize-PowerShellEnviroment}.ToString()
        $funcDef_UpdateWindowsSystem = ${function:Update-WindowsSystem}.ToString()
        $funcDef_UpdateCourseVM = ${function:Update-CourseVM}.ToString()
        $ErrorActionPreference = 'Stop'
        $ConsoleHighlightColor = "Green"

        if ($Host.Version.Major -lt 7) {
            throw "this function is only supported in powershell 7, use Update-CourseVM if you want to update individual vms."
        }

        # collect VM info
        try {
            $VMs = Get-VM | Where-Object -Property Path -like "$($env:SystemDrive)\ClusterStorage\VMs\$($VMFolderName)\*"
            if ($null -eq $VMs) {
                throw "no vms were found"
            }

            $SelectedVMs = $VMs | ForEach-Object {
                $VMOS = Get-VMOperatingSystem -VMName $PSItem.Name
                if ($null -eq $VMOS -or $VMOS -eq "") {
                    Write-Warning "could not determine vm os, skipping this vm $($PSItem.Name)"
                }
                elseif ($VMOS -notlike "*Windows*") {
                    Write-Warning "vm $($PSItem.Name) has not windows, skipping"
                }
                elseif ($VMOS -like "*Windows*") {
                    Write-Verbose "vm $($PSItem.Name) has windows"
                    return $PSItem
                }
            }
        }
        catch {
            throw "could not fetch vms - $($PSItem.Exception.Message)"
        }
    }

    process {
        if ($ShowOnly) {
            Write-Host "this would trigger updates on those vms" -ForegroundColor $ConsoleHighlightColor
            $VMs.Name
        }
        else {
            if ($SelectedVMs.Count -eq 1) {
                Update-CourseVM -VMName $SelectedVMs.Name -VMCredential $VMCredential -AutoReboot $AutoReboot
            }
            elseif ($null -ne $SelectedVMs) {
                # https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/write-progress-across-multiple-threads?view=powershell-7.3#full-example
                # Create a hashtable for process.
                # Keys should be ID's of the processes
                $origin = @{}
                $SelectedVMs | Foreach-Object {
                    $origin.($PSItem.Id) = @{}
                }
        
                # Create synced hashtable
                $sync = [System.Collections.Hashtable]::Synchronized($origin)
        
                Write-Progress -Id 0 -Activity "updating vms" -Status "starting"
                Start-Sleep -Seconds 1
        
                $Jobs = $SelectedVMs | Foreach-Object -ThrottleLimit 5 -AsJob -Parallel {
                    $syncCopy = $using:sync
                    $process = $syncCopy.$($PSItem.Id)
        
                    $process.ParentId = 0
                    $process.Id = $($using:SelectedVMs).IndexOf($PSItem) + 1
                    $process.Activity = "VMName $($PSItem.Name)"
                    $process.Status = "start update process"
                
                    # Fake workload start up that takes x amount of time to complete
                    Start-Sleep -Milliseconds ($PSItem.wait * 5)
                
                    #region do stuff
                    $process.Status = "start update process, connecting"
                    Start-Sleep -Seconds 1
                
                    # Define the function inside this thread from parent thread
                    ${function:Initialize-PowerShellEnviroment} = $using:funcDef_InitializePowerShellEnviroment
                    ${function:Update-WindowsSystem} = $using:funcDef_UpdateWindowsSystem
                    ${function:Update-CourseVM} = $using:funcDef_UpdateCourseVM
        
                    try {
                        Update-CourseVM -VMName $PSItem.Name -VMCredential $using:VMCredential -AutoReboot $using:AutoReboot -Verbose
                    }
                    catch {
                        $PSItem.Exception.Message
                    }

                    $process.Status = "finished update process, $(if ($using:AutoReboot -eq $true){ "rebooting vm" })"
                    Start-Sleep -Seconds 3
                    # Start-Sleep -Seconds 10
                    #endregion
        
                    # Mark process as completed
                    $process.Completed = $true
                }
                
                while ($Jobs.State -eq 'Running') {
                    # get percentage of completed
                    $PercentageCompleted = [Math]::Round([Math]::Ceiling(($sync.Values | Where-Object -Property Completed -eq $true).Count / $VMs.count * 100), 2)
                    $sync.Keys | Foreach-Object {
                        # If key is not defined, ignore
                        if (![string]::IsNullOrEmpty($sync.$PSItem.keys)) {
                            # Create parameter hashtable to splat
                            $param = $sync.$PSItem
        
                            # Execute Write-Progress
                            Write-Progress -Id 0 -Activity "updating vms" -Status "running" -PercentComplete $PercentageCompleted
                            Write-Progress @param
                        }
                    }
                
                    # Wait to refresh to not overload gui
                    Start-Sleep -Seconds 0.3
                }
        
                Write-Progress -Id 0 -Activity "updating vms" -Status "Finished" -Completed
            }
        }
    }
}

function Get-VMOperatingSystem {
    # https://stackoverflow.com/questions/38096777/is-there-a-way-to-get-vms-operating-system-name-from-hyper-v-using-powershell

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $VMName
    )
    try {
        # Check if VM exists and is running. This script doesn't work if the VM is stopped.
        # Capture error output, source: https://stackoverflow.com/a/66861283/3498768
        $vm_not_found = $($vm_state = (Get-VM $VMName).state) 2>&1

        if ($null -ne $vm_not_found) {
            throw "$VMName VM was not found."
        }

        if ($vm_state -eq "Off") {
            Write-Warning "Cannot retrieve information of $VMName. The VM is stopped. Only running VM information can be retrieved."
        }
        else {
            # Get the virtual machine object
            $vm = Get-CimInstance -namespace "root\virtualization\v2" -query "Select * From Msvm_ComputerSystem Where ElementName='$($VMName)'"

            # Get associated information
            $vm_info = Get-CimAssociatedInstance -InputObject $vm

            # Select only required information
            $OS = ($vm_info | Where-Object GuestOperatingSystem).GuestOperatingSystem
            if ($null -ne $OS) {
                return $OS
            }
            else {
                throw "os info not found"
            }
        }
    }
    catch {
        throw "could not find data - $($PSItem.Exception.Message)"
    }
}