NTS-ConfigMgrTools.psm1

function New-VMVolume {
    param (
        # Driveletter
        [Parameter(Mandatory = $true)]
        [char]
        $VMDriveLetter,

        # StoragePool Name
        [Parameter(Mandatory = $true)]
        [string]
        $StoragePoolName,

        # VirtualDisk Name
        [Parameter(Mandatory = $true)]
        [string]
        $VirtualDiskName,

        # VMVolume Name
        [Parameter(Mandatory = $true)]
        [string]
        $VMVolumeName
    )

    $LogPath = "$($env:ProgramData)\NTS\New-VMVolume-$(Get-Date -format "yyyy-MM-dd_HH.mm.ss").log"
    New-Item -Path $LogPath -ItemType File -Force | Out-Null
    Start-Transcript -Append $LogPath | Out-Null

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


function New-VMVSwitch {
    param (
        # Course Shortcut
        [Parameter(Mandatory = $true)]
        [string]
        $Course_Shortcut
    )

    $LogPath = "$($env:ProgramData)\NTS\New-VMVSwitch-$(Get-Date -format "yyyy-MM-dd_HH.mm.ss").log"
    New-Item -Path $LogPath -ItemType File -Force | Out-Null
    Start-Transcript -Append $LogPath | Out-Null
    
    try {
        if($null -eq (Get-VMSwitch -Name $Course_Shortcut -ErrorAction SilentlyContinue)){
            $pNICs = Get-NetAdapter -Physical | Where-Object -Property Status -eq "UP"
            $10G_NICs = $pNICs | Where-Object -Property LinkSpeed -EQ "10 Gbps"
            $1G_NICs = $pNICs | Where-Object -Property LinkSpeed -EQ "1 Gbps"
    
            if($10G_NICs){
                $Selected_NIC = $10G_NICs[0]
            }
            elseif($1G_NICs){
                $Selected_NIC = $1G_NICs[0]
            }
            else {
                (Get-NetAdapter -Physical | Where-Object -Property Status -eq "UP")[0]
            }
    
            Write-Output "create vswitch '$($Course_Shortcut)'"
            New-VMSwitch -Name $Course_Shortcut -NetAdapterName $Selected_NIC.Name -AllowManagementOS $false | Out-Null
            Add-VMNetworkAdapter -ManagementOS -SwitchName $Course_Shortcut -Name "vNIC-$($Course_Shortcut)"
            Rename-NetAdapter -Name $Selected_NIC.Name -NewName "pNIC-$($Course_Shortcut)"
        }
        else{
            Write-Output "virtual vswitch '$($Course_Shortcut)' already exists"
        }
    }
    catch {
        Write-Output "error during creation of virtual switch: $($PSItem.Exception.Message)"
        Stop-Transcript | Out-Null
    }
}
Export-ModuleMember -Function New-VMVSwitch


function Register-VM_in_CM {
    [CmdletBinding()]
    param (
        # VM Object
        [Parameter()]
        [hashtable]
        $VM_Config_Obj,

        # ConfigMgr SMS Provider FQDN
        [Parameter()]
        [string]
        $CM_Siteserver_FQDN,

        # Credentials for PSRemoting to SMS Provider and permissions to configmgr
        [Parameter()]
        [PSCredential]
        $CM_Credentials
    )

    try {
        Invoke-Command -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials -ScriptBlock {
            $PSDriveName = "CM-PB2"
            $VM_Config_Obj = $using:VM_Config_Obj

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

            foreach ($VM in $VM_Config_Obj.Keys | Sort-Object){
                # check destination collection existance
                if ($null -eq (Get-CMCollection -Name $VM_Config_Obj.$VM.CM_Collection_Name)) {
                    Get-CMDevice -Name $VM | Remove-CMDevice -Force -Confirm:$false
                    throw "collection '$($VM_Config_Obj.$VM.CM_Collection_Name)' does not existing, device infos for '$($VM)' was removed"
                }
            }
            
            # create vm info
            foreach ($VM in $VM_Config_Obj.Keys | Sort-Object){
                Write-Output "'$($VM)': creating vm computer info in configmgr - macaddress '$($VM_Config_Obj.$VM.MAC)'"
                Get-CMDevice -Name $VM | Remove-CMDevice -Force -Confirm:$false
            }
            Start-Sleep -Seconds 5
            foreach ($VM in $VM_Config_Obj.Keys | Sort-Object){
                Import-CMComputerInformation -CollectionName "All Systems" -ComputerName $VM -MacAddress $VM_Config_Obj.$VM.MAC
            }

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

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


function Start-VM_Deployment {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [hashtable]
        $VM_Config_Obj
    )

    try {
        foreach ($VM in $VM_Config_Obj.Keys | Sort-Object) {
            Write-Output "'$($VM)': starting deployment"
            Start-Sleep -Seconds 2
            Start-VM -VMName $VM
        }
    }
    catch {
        throw "error while starting vms: $($PSItem.Exception.Message)"
    }
}
Export-ModuleMember -Function Start-VM_Deployment


function Confirm-VM_Deployment {
    [CmdletBinding()]
    param (
        # VM Object
        [Parameter(Mandatory = $true)]
        [hashtable]
        $VM_Config_Obj,

        # ConfigMgr SMS Provider FQDN
        [Parameter()]
        [string]
        $CM_Siteserver_FQDN,

        # Credentials for PSRemoting to SMS Provider and permissions to configmgr
        [Parameter()]
        [PSCredential]
        $CM_Credentials
    )

    try {
        Invoke-Command -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials -ScriptBlock {
            $PSDriveName = "CM-PB2"

            Import-Module -Name ConfigurationManager
            New-PSDrive -Name $PSDriveName -PSProvider "CMSite" -Root $using:CM_Siteserver_FQDN -Description "Primary site" | Out-Null
            Set-Location -Path "$($PSDriveName):\"
    
            $Deployment_Finished = $false
            $CM_Deployment_Running = @{}
            foreach ($VM in $using:VM_Config_Obj.keys | Sort-Object) {
                $CM_Deployment_Running.Add($VM,$true)
            }
    
            do {
                $CM_All_Deployments = Get-CMDeploymentStatus | Get-CMDeploymentStatusDetails | `
                                        Where-Object -FilterScript { $using:VM_Config_Obj.keys -contains $PSItem.Devicename } | `
                                        Sort-Object -Property DeviceName
    
                if ($null -ne $CM_All_Deployments) {
                    foreach($VM_Deployment in $CM_All_Deployments){
                        if($VM_Deployment.StatusDescription -notlike "*The task sequence manager successfully completed execution of the task sequence*"){
                            Write-Output "'$($VM_Deployment.DeviceName)': still running - $($VM_Deployment.StatusDescription)"
                            $CM_Deployment_Running.$($VM_Deployment.Devicename) = $true
                        }
                        else {
                            Write-Output "'$($VM_Deployment.Devicename)': finished - $($VM_Deployment.StatusDescription)"
                            $CM_Deployment_Running.$($VM_Deployment.Devicename) = $false
                        }
                    }
                }
                else {
                    Write-Output "Waiting on Deployments"
                }
                Write-Output "------"
    
                if($CM_Deployment_Running.Values -notcontains $true){
                    $Deployment_Finished = $true
                }
                else {
                    Start-Sleep -Seconds 15
                }
    
            } while ($Deployment_Finished -eq $false)
    
            Write-Output "vm os deployment completed"
            Set-Location -Path $env:SystemDrive
            Remove-PSDrive -Name $PSDriveName
        }    
    }
    catch {
        throw "error while checking deployment status: $($PSItem.Exception.Message)"
    }
}
Export-ModuleMember -Function Confirm-VM_Deployment


function Deploy-VMBasedOnObject {
    param (
        # VM Object
        [Parameter(Mandatory = $true)]
        [System.Object]
        $VM_Config,

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

        # ConfigMgr SMS Provider FQDN
        [Parameter()]
        [string]
        $CM_Siteserver_FQDN,

        # Credentials for PSRemoting to SMS Provider and permissions to configmgr
        [Parameter()]
        [PSCredential]
        $CM_Credentials,

        # VM Drive Letter
        [Parameter(Mandatory = $true)]
        [char]
        $VMDriveLetter
    )
    
    # VM Creation
    $VM_Deployment_Start = Get-Date
    $LogPath = "$($env:ProgramData)\NTS\Deploy-VMBasedOnObject-$(Get-Date -format "yyyy-MM-dd_HH.mm.ss").log"
    New-Item -Path $LogPath -ItemType File -Force | Out-Null
    Start-Transcript -Append $LogPath | Out-Null

    try {
        $VM_Base_Path = $VMDriveLetter + ":\VMs"
        Write-Output "`nstarting vm creation"
        Write-Output "------"
        foreach($VM in $VM_Config.Keys | Sort-Object){
            $VMVHDXPath = ($VM_Base_Path + "\" + $VM_Config.$VM.Name + "\" + $VM_Config.$VM.Name + ".vhdx")
            Write-Output "'$($VM_Config.$VM.Name)': creating vm"
            try {
                New-VHD -Path $VMVHDXPath -SizeBytes $VM_Config.$VM.DiskSize -Dynamic | Out-Null
                New-VM -Name $VM_Config.$VM.Name -MemoryStartupBytes $VM_Config.$VM.RAM -Path $VM_Base_Path -Generation 2 -VHDPath $VMVHDXPath -BootDevice NetworkAdapter -SwitchName $Course_Shortcut | Out-Null
            }
            catch {
                throw "error during creation of vhdx or vm '$($VM_Config.$VM.Name)'"
                Stop-Transcript | Out-Null
            }
            try {
                Set-VMProcessor -VMName $VM_Config.$VM.Name -Count $VM_Config.$VM.CPU
                Set-VMKeyProtector -VMName $VM_Config.$VM.Name -NewLocalKeyProtector
                Enable-VMTPM -VMName $VM_Config.$VM.Name
                Set-VM -AutomaticStartAction Nothing -VMName $VM_Config.$VM.Name
                Set-VM -AutomaticStopAction ShutDown -VMName $VM_Config.$VM.Name
                Get-VMIntegrationService -VMName $VM_Config.$VM.Name | Where-Object -Property Enabled -EQ $false | Enable-VMIntegrationService
            }
            catch {
                throw "error while setting properties of vm '$($VM_Config.$VM.Name)'"
                Stop-Transcript | Out-Null
            }
            Start-VM -Name $VM_Config.$VM.Name
            Start-Sleep -Seconds 2
            Stop-VM -Name $VM_Config.$VM.Name -Force -TurnOff
            Start-Sleep -Seconds 1
        }
        foreach($VM in $VM_Config.Keys | Sort-Object){
            $VM_Config.$VM.MAC = (Get-VM -Name $VM_Config.$VM.Name | Get-VMNetworkAdapter).MacAddress
            Set-VMNetworkAdapter -VMName $VM_Config.$VM.Name -StaticMacAddress $VM_Config.$VM.MAC
        }
    }
    catch {
        throw "error during creation of vms: $($PSItem.Exception.Message)"
        Stop-Transcript | Out-Null
    }

    # ConfigMgr / Deployment
    try {
        Write-Output "`nstarting configmgr preparation"
        Write-Output "------"
        Register-VM_in_CM -VM_Config_Obj $VM_Config `
                        -CM_Siteserver_FQDN $CM_Siteserver_FQDN `
                        -CM_Credentials $CM_Credentials
        Write-Output "`nfinished configmgr preparation, now waiting 90 seconds for the configmgr database updates and stabilization"
        Start-Sleep -Seconds 180
        Write-Output "`nstarting vm deployments"
        Write-Output "------"
        Start-VM_Deployment -VM_Config_Obj $VM_Config
        
        Write-Output "`nchecking vm os deployment status"
        Write-Output "------"
        Confirm-VM_Deployment -VM_Config_Obj $VM_Config `
                            -CM_Siteserver_FQDN $CM_Siteserver_FQDN `
                            -CM_Credentials $CM_Credentials
        Write-Output "`nfinished vm deployments"
        Write-Output "------"
    }
    catch {
        throw "error during vm deployment: $($PSItem.Exception.Message)"
        Stop-Transcript | Out-Null
    }
    $VM_Deployment_Duration = (Get-Date) - $VM_Deployment_Start
    Write-Output "vm deployment took $($VM_Deployment_Duration.Hours)h $($VM_Deployment_Duration.Minutes)m $($VM_Deployment_Duration.Seconds)s"
    Stop-Transcript | Out-Null
}
Export-ModuleMember -Function Deploy-VMBasedOnObject


function Test-VMConnection {
    [cmdletbinding()]
    param (
        # VM object id
        [Parameter(Mandatory = $true)]
        [Guid]
        $VMId,

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

    $LogPath = "$($env:ProgramData)\NTS\Test-VMConnection-$(Get-Date -format "yyyy-MM-dd_HH.mm.ss").log"
    New-Item -Path $LogPath -ItemType File -Force | Out-Null
    Start-Transcript -Append $LogPath | Out-Null
    $VM = Get-VM -Id $VMId

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

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