Working with Hyper-V VMs in an Intune Lab environment

As a lot of my blog readers probably know :-), I’m working a lot with Microsoft Endpoint Manager – Intune and testing a lot of things in the Modern Management approach with Windows 10. It is absolutely necessary to have a good lab setup to test all these new features in a save way. For this I will share my approach how to work in my lab setup. First of all you need a performant Hyper-V Server with enough memory to support your test server/devices. I’m using a Server with an Intel i7 processor with 4 Cores and 64 GB of memory. Make sure to have a good SSD to provide good throughput especially when testing all these enrollments this is the best way to speed things up. This post is not about choosing the best hardware setup. An important fact is to make sure to enable Data Deduplication on your VM data drive to store the VM data files (vhdx) efficiently. Several Windows 10 .vhdx files can be stored very efficiently, as they all have a large portion of the same data in it (around 80-95% space saving for this data type). The setup I described above, brings a fair amount of power and throughput to work with 10 devices in parallel.

Now when it comes to work with the VMs I follow a few simple rules and have a few support utilities and scripts always available. All scripts can be found on my GitHub account here:

https://github.com/okieselbach/Intune

  1. I use Hyper-V native VM Connect and not the “Enhanced Session Mode“, as this blocks the capability to test Windows Hello for Business within my VM, I know I give up convenience in regards of copy paste, but I can easily live with that, as I have other options which are for me sufficient enough:
Hyper-V enhanced session mode disabld
  1. I create my VMs by using a PowerShell script to speed up the process of deploying a new device. This script makes sure to start my VM via VM Connect right after creation and with a higher resolution from the beginning:
Hyper-V VM Connect with high resolution

The Virtual TPM in Hyper-V is enabled to support features like BitLocker encryption and other security settings within the VM:

Hyper-V VM settings for Security, Virtual TPM, and Secure Boot

The script asks me if I want to use the latest Windows ISO or an Insider ISO file. Following the PowerShell script Create-MyVM.ps1 which I use on my Hyper-V host:

<#
Version: 1.0
Author: Oliver Kieselbach (oliverkieselbach.com)
Script: Create-MyVM.ps1
Description:
The script crates a VM on a Hyper-V host with TPM and starts it including the VMConnect client.
Release notes:
Version 1.0: Original published version.
The script is provided "AS IS" with no warranties.
#>
# ask some parameters like VM name, CPU count or latest Win10 or Insider
$VMName = Read-Host Prompt 'Enter VM name'
if (($CPUCount = Read-Host Prompt "CPU count? [default=4, Enter]") -eq "") { $CPUCount = 4 }
if (($Insider = Read-Host Prompt "Insider? [default=no, Enter]") -eq "") { $Insider = $false }
else { $Insider = $true }
if (!$Insider) {
$IsoPath = "D:\en_windows_10_business_editions_version_20h2_x64_dvd_4788fb7c.iso"
}
else {
$IsoPath = "D:\20246.1.201024-2009.fe_release_CLIENT_BUSINESS_VOL_x64FRE_en-us.iso"
}
# some definitions for Network and VM storage path
$VMSwitchName = "Private (Class C)"
$VhdxPath = "V:\Hyper-V\Virtual Hard Disks\$VMName.vhdx"
$VMPath = "V:\Hyper-V\Virtual Machines"
# I'm not usign Enhanced Session Mode, so we can run this once to disable it on the host
#Set-VMHost -EnableEnhancedSessionMode $false
New-VM Name $VMName BootDevice VHD NewVHDPath $VhdxPath Path $VMPath NewVHDSizeBytes 127GB Generation 2 Switch $VMSwitchName
Set-VM VMName $VMName ProcessorCount $CPUCount
Set-VMMemory VMName $VMName StartupBytes 2GB MinimumBytes 512MB MaximumBytes 1048576MB DynamicMemoryEnabled $true
Set-VMSecurity VMName $VMName VirtualizationBasedSecurityOptOut $false
Set-VMKeyProtector VMName $VMName NewLocalKeyProtector
Enable-VMTPM VMName $VMName
Add-VMDvdDrive VMName $VMName ControllerNumber 0 ControllerLocation 1
Set-VMDvdDrive VMName $VMName Path $IsoPath
$bootorder = (Get-VMFirmware VMName $VMName).BootOrder
Set-VMFirmware VMName $VMName BootOrder $bootorder[2],$bootorder[0],$bootorder[1]
Enable-VMIntegrationService VMName $VMName Name "Guest Service Interface"
# To test out stuff which relies on hypervisor etc. uncomment the following line
#Set-VMProcessor -VMName $VMName -ExposeVirtualizationExtensions $true
Set-VMVideo VMName $VMName ResolutionType Single HorizontalResolution 1920 VerticalResolution 1080
# if a teh default is wanted use:
#Set-VMVideo -VMName $VMName -ResolutionType Default
# Start the CM right away and open the VMConnect window to directly inteact with the new VM
Start-VM Name $VMName
VMConnect localhost $VMName
view raw Create-MyVM.ps1 hosted with ❤ by GitHub

This is convenient enough for me to fire up VMs quickly, in a very clean state, as I let them run through the regular Windows Setup from the mounted ISO.

  1. I normally do not reset devices as the Windows Factory Reset aka Push Button Reset (PBR) re-creates the Windows files/folder from the side-by-side cache which takes a fairly long time. With a Hyper-V server and enough throughput I’m always faster in just mounting the ISO again and re-install the OS
  2. I use the Hyper-V VM integration services and a simple script to copy files into my VM if needed
PowerShell script Copy-VMFile
Transfer folder structure

Here the script used for coping into the VM Copy-VMFile.ps1:

$VMName = Read-Host -Prompt 'Enter VM name'
$FileName = Read-Host -Prompt 'Enter FileName'
Copy-VMFile $VMName -SourcePath "D:\$FileName" -DestinationPath "C:\Tools\$FileName" -CreateFullPath:$true -FileSource Host
  1. I use a simple network share from my Hyper-V host to provide the ability to transfer files out from the VM. For that I use a small PowerShell script to create the batches on the fly and deploy them to C:\Windows\System32 (System32 is used to have the batches included in the path variable) to support me executing them easily. This way I have a way to simply type connect (connect.cmd) and disconnect (disconnect.cmd) on the command line to establish the share connection:
command prompt with connect and disconnect batch file

The PowerShell script Create-TransferBatches.ps1 used in Intune to generate the two batches is that:

$content = "net use Z: \\192.168.1.1\Transfer$ /user:Administrator"
Out-File -FilePath "$env:windir\System32\connect.cmd" -Encoding ascii -InputObject $content -Force:$true
$content = "net use Z: /delete /y"
Out-File -FilePath "$env:windir\System32\disconnect.cmd"  -Encoding ascii -InputObject $content -Force:$true
  1. I use the classic cmtrace utility from Microsoft (bundled in the ConfigMgr Toolkit) to support Intune Management Extension (IME) log file reading. I have a dedicated .intunewin package to deploy it to C:\Windows\System32. As a goodie it has set the most recent used file entries (MRU) for the common IME log files:
cmtrace with MRU entries for Intune Management Extension (IME) log files

To install it I use this small install batch file. You need to be careful when copying it over to System32 from a 32-bit context (IME app installs run by default in 32-bit).

::we are running in a 32-bit process so copy to system32 is different!
cmd /c copy /y cmtrace.exe %windir%\Sysnative
powershell -ex bypass -file .\Create-CmtraceMRU.ps1

The Create-CmtraceMRU.ps1 modifies the Most Recently Used (MRU) registry entries to point to the Intune Management Extension (IME) log files for easy access:

<#
Author: Oliver Kieselbach (oliverkieselbach.com)
Script: Create-CmtraceMRU.ps1
The script is provided "AS IS" with no warranties.
#>
# write cmtrace MRU list for SYSTEM user
& REG DELETE HKCU\Software\Microsoft\Trace32 /f /reg:64 | Out-Null
& REG ADD HKCU\Software\Microsoft\Trace32 /v "Register File Types" /t REG_SZ /d "1" /f /reg:64 | Out-Null
& REG ADD HKCU\Software\Microsoft\Trace32 /v "Maximize" /t REG_SZ /d "1" /f /reg:64 | Out-Null
& REG ADD HKCU\Software\Microsoft\Trace32 /v "Last Directory" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs" /f /reg:64 | Out-Null
& REG ADD HKCU\Software\Microsoft\Trace32 /v "MRU0" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log" /f /reg:64 | Out-Null
& REG ADD HKCU\Software\Microsoft\Trace32 /v "MRU1" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\AgentExecutor.log" /f /reg:64 | Out-Null
& REG ADD HKCU\Software\Microsoft\Trace32 /v "MRU2" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\Sensor.log" /f /reg:64 | Out-Null
& REG ADD HKCU\Software\Microsoft\Trace32 /v "MRU3" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\ClientHealth.log" /f /reg:64 | Out-Null
# as we run this early in the process during OOBE as SYSTEM, there is the defaultuser* (it's not always defaultuser0 it can also be defaultuser1 etc.)
# already used, so we write the MRU explicitly in the evaluated defaultuser* user hive to get our custom cmtrace MRU list in OOBE
$defaultUserXSid = (Get-WmiObject win32_userprofile | Select-Object LocalPath,SID | Where-Object LocalPath -like "$env:SystemDrive\users\defaultuser*").SID
& REG DELETE HKU\$defaultUserXSid\Software\Microsoft\Trace32 /f /reg:64 | Out-Null
& REG ADD HKU\$defaultUserXSid\Software\Microsoft\Trace32 /v "Register File Types" /t REG_SZ /d "0" /f /reg:64 | Out-Null
& REG ADD HKU\$defaultUserXSid\Software\Microsoft\Trace32 /v "Maximize" /t REG_SZ /d "1" /f /reg:64 | Out-Null
& REG ADD HKU\$defaultUserXSid\Software\Microsoft\Trace32 /v "Last Directory" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs" /f /reg:64 | Out-Null
& REG ADD HKU\$defaultUserXSid\Software\Microsoft\Trace32 /v "MRU0" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log" /f /reg:64 | Out-Null
& REG ADD HKU\$defaultUserXSid\Software\Microsoft\Trace32 /v "MRU1" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\AgentExecutor.log" /f /reg:64 | Out-Null
& REG ADD HKU\$defaultUserXSid\Software\Microsoft\Trace32 /v "MRU2" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\Sensor.log" /f /reg:64 | Out-Null
& REG ADD HKU\$defaultUserXSid\Software\Microsoft\Trace32 /v "MRU3" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\ClientHealth.log" /f /reg:64 | Out-Null
# Load default user ntuser.dat in tempDefault if not already loaded
$tempProfileName = "tempDefaultUser"
If (($profileLoaded = Test-Path Registry::HKEY_USERS\$tempProfileName) -eq $false) {
& REG.EXE LOAD HKU\$tempProfileName "$env:SystemDrive\Users\Default\NTuser.dat" | Out-Null
}
# write cmtrace MRU list for default user
& REG DELETE HKU\$tempProfileName\Software\Microsoft\Trace32 /f /reg:64 | Out-Null
& REG ADD HKU\$tempProfileName\Software\Microsoft\Trace32 /v "Register File Types" /t REG_SZ /d "1" /f /reg:64 | Out-Null
& REG ADD HKU\$tempProfileName\Software\Microsoft\Trace32 /v "Maximize" /t REG_SZ /d "1" /f /reg:64 | Out-Null
& REG ADD HKU\$tempProfileName\Software\Microsoft\Trace32 /v "Last Directory" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs" /f /reg:64 | Out-Null
& REG ADD HKU\$tempProfileName\Software\Microsoft\Trace32 /v "MRU0" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log" /f /reg:64 | Out-Null
& REG ADD HKU\$tempProfileName\Software\Microsoft\Trace32 /v "MRU1" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\AgentExecutor.log" /f /reg:64 | Out-Null
& REG ADD HKU\$tempProfileName\Software\Microsoft\Trace32 /v "MRU2" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\Sensor.log" /f /reg:64 | Out-Null
& REG ADD HKU\$tempProfileName\Software\Microsoft\Trace32 /v "MRU3" /t REG_SZ /d "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\ClientHealth.log" /f /reg:64 | Out-Null
# Unload default user NTuser.dat
If ($profileLoaded -eq $false) {
[gc]::Collect()
Start-Sleep 1
& REG.EXE UNLOAD HKU\$tempProfileName | Out-Null
}
  1. I use another simple batch file called imesync.cmd which I also distribute via Intune PowerShell script to C:\Windows\System32. It gives me easy access to sync the Intune Management Extension (IME) by just typing imesync in the command prompt:
Windows run dialog with imesync batch fils

To create the supporting batch file I deploy this small PowerShell script Create-ImeSyncBatch.ps1 via Intune which creates the imesyc.cmd:

$content = "powershell -Ex bypass -Command `"& {`$Shell = New-Object -ComObject Shell.Application ; `$Shell.open('intunemanagementextension://syncapp')}`""
Out-File -FilePath "$env:windir\System32\imesync.cmd" -Encoding ascii -InputObject $content -Force:$true
  1. I use a dedicated intunewin packages to populate the folder C:\Tools with the super helpful Sysinternals Suite tools within my VM:
C:\Tools folder with common troubleshooting tools, SyncMLViewer, Sysinternals and WMIExplorer

My Sysinternals Suite .intunewin package has also a sudo.cmd which is also copied to C:\Windows\System32 for easy access to a system context command prompt by using the Sysinternals tool psexec. This is normally available very early, so I can even use it in OOBE and [Shift] + [F10] and just type in sudo to get an command prompt running in system context with the C:\Tools folder as working directory. I know in OOBE Iā€˜m already System but I want to show how early everything is available:

sudo batch file which automates psexec execution to run command prompt in system context

This is my sudo.cmd file:

C:\Tools\Sysinternals\PsExec.exe -accepteula -sid cmd.exe "/k cd C:\Tools &&^
 echo current execution context: && echo. && whoami && title %computername%"

I use this simple install batch file for copying the Sysinternal Suite and the sudo file to C:\Windows\System32. You need to be careful when copying it over to System32 from a 32-bit context (IME app installs run by default in 32-bit).

xcopy /hericy * C:\Tools\Sysinternals\
::we are running in a 32-bit process so copy to system32 is different!
cmd /c copy /y sudo.cmd %windir%\Sysnative
  1. I have a SyncML Viewer intunewin package which copies the SyncML Viewer under C:\Tools to easily trace the MDM protocol:
SyncMLViewer running on desktop user session

A short note, the SyncML Viewer can also be run in OOBE even when I’m showing it here on the regular desktop. To learn more about the SyncML Viewer see my article here: Windows 10 MDM client activity monitoring with SyncML Viewer

  1. A tool which can’t be missed is the WMI Explorer to work easily with WMI:
WMIExplorer running on desktop user session in a system context started by pseexec system context command prompt

Once again WMI Explorer can also be used during OOBE and [Shift] + [F10]. In my picture you can see I used my sudo.cmd to get my system command prompt to finally open WMI Explorer to get access to all WMI instances.

  1. As already seen several times in the screenshots, to easily identify my VMs I have a custom BGInfo background to print the most important parameters to the upper right of the background, VM hostname, Windows version info and if the user is Admin or Standard user:
Windows 10 BGInfo details printet on the upper right Windows Desktop

To learn more about that see my article here: Deploying Win32 app BGInfo with Intune

Summary

I have shown my simple setup to work efficiently with VMs with a minimal amount of maintenance or preparation to setup all this. Currently I already live with this setup for a long time and very well. Once again here my tool set summary:

  1. Easy VM creation by using Create-MyVM.ps1
  2. Easy Copy into VMs by using Copy-VMFile.ps1
  3. cmtrace in C:\Windows\System32 for IME log files (link)
  4. imesync in C:\Windows\System32 for easy IME sync trigger (link)
  5. connect or disconnect to open network share to VM Host (link)
  6. sudo for easy elevation to System context via psexec
  7. under C:\Tools we have:
    • Sysinternals Tools
    • SyncML Viewer
    • WMIExplorer
  8. Custom BGInfo to easily recognize my VM parameters (link)

These are not many things in total but they are giving me flexibility and a lab environment to work efficiently and test or troubleshoot the technology.

I hope I could inspire one or the other to organize his lab a bit or to post new ideas in the comment below.

Thanks for reading and maybe you got some new efficient ways to work šŸ™‚