Intro

Windows Autopilot has become more and more popular. This trend has been accelerated by the pandemic situation when companies are required to find solutions to allow their employees to work remote

After completing this post your environment will look like this

Challenge

You need to find a way to create autopilot devices for your demos. You do not want to use local infrastructure (local PC). Instead you want to create this in Azure

Solution

Deploy a Hyper-V host with a nested autopilot device in Azure using an ARM template. Once the infrastructure is ready, collect the autopilot HWID, register the autopilot device and wipe the device to bring it back to the OOBE screen

Requirements

For this demo you will need the following

  • Contributor on an Azure Subscription – to create the Hyper-V host
  • M365 Tenant with global administrator account – this can be a trial account
  • Existing autopilot configuration  – see this post
    • Autopilot Profile
    • Dynamic Group
    • Enable MDM auto-enrollment
  • Prepared Win 10 image – see this post

Steps

The main steps we will follow in this demo are the following:

  1. Upload image to cloud
  2. Create application registration
  3. Clone repository and modify scripts if needed
  4. Deploy template
  5. Validate deployment

Upload Image to Cloud

Note: you can skip this step if you want to use the image that I created

  • Prepare an image as show in this post.
  • Compress the image with 7zip
  • Use the below script to create storage infrastructure (storage account, and container with public read access to blobs) in Azure to hold your image. Change the variables based on your options
location = "westeurope"
$RGName = "Templates-RG"
$sAccountName = "youraccount"
$containerName = "yourcontainer"

New-AzResourceGroup -Name $RGName -Location $location

$context = New-AzStorageAccount -ResourceGroupName $RGName -name $sAccountName -Location $location -skuname Standard_LRS

New-AzStorageContainer -name $containerName -Context $context.Context -Permission blob 
  • Upload the image in the container (using Azure Storage Explorer or azcopy) and capture the URL

Create Application Registration

To automatically register Autopilot devices, we require access to Microsoft Graph using PowerShell. For this we will create an application with access to what we need. We need to capture the application object ID and secret for authentication. Microsoft Graph permissions for Autopilot devices are under DeviceManagementServiceConfig

  • Login to Azure portal with a global administrator account
  • In Azure Active directory select app registration and then new registration
  • Create the application in your tenant only
  • Once created make sure you capture the object ID

Select API permissions > Add a permission > Microsoft Graph > Application Permission then DeviceManagementServiceConfig.ReadWrite.All and add this permission

  • Grant Admin consent for permission
  • Select Certificates & secrets and create a new certificate. Make sure you save the secret in a text editor before leaving the page

Clone repository and modify scrips

We now have the image and the application registration to connect to Microsoft Graph. It is time to look at the scripts located here

Open git desktop and clone the repository in a local folder

git clone https://github.com/gabim101/AutoPilotDemo

This repository requires a bit of explanation.

We have an ARM template (based on Microsoft quick start templates) that deploys a hyper V host (Windows Server 2016). Once the VM is ready, the template executes an PowerShell extension that does the following

  • Installs the required modules
  • Creates the Hyper-V infrastructure
  • Downloads and extracts the golden image
  • Connects to MS Graph on the tenant that has the Autopilot configuration and will host the Autopilot device
  • Creates a nested Windows 10 VM inside the Hyper-V host based on the golden image
  • Starts a PowerShell Session with the nested VM once it is up
  • Executes the script to collect the Autopilot HWID
  • Uploads the HWID IT to cloud to import the device and waits for profile assignment
  • resets the nested win 10 VM once the profile is assigned

Note the parameters the script requires. Here we pass the app registration credentials and golden image URL.

Below is the script. Note it has the password I created for golden image. Update the script with your own password if required. However, if you do this you will need to save the script to your location and update the _artifactsLocation parameter in azuredeploy.json

[cmdletbinding()]
param (
    [string]$NIC1IPAddress,
    [string]$NIC2IPAddress,
    [string]$GhostedSubnetPrefix,
    [string]$VirtualNetworkPrefix,
    [string]$ClientID,
    [string]$ClientSecret,
    [string]$Tenant,
    [string]$imageUrl
)

function Create-VM {
<#
.SYNOPSIS
This function is used to create a hyper-v VM
.DESCRIPTION
Create a Hyper-V VM. Change the parameters inside the function to customize the VM. At the end of the fucntion TPM is set and
boot disk is replace with gold image
#>
param
(
    [Parameter(Mandatory=$true)][string]$VMN,
    [parameter(Mandatory=$true)][string]$bootIMG
)

$NewVMParam = @{
  Name = $VMN
  Generation = 2
  MemoryStartUpBytes = 1GB
  Path = "c:\VMs"
  SwitchName =  "NestedSwitch"
  NewVHDPath =  "f:\VMs\$VMN\boot.vhdx"
  NewVHDSizeBytes =  50GB 
  ErrorAction =  'Stop'
  Verbose =  $True
  }

  $SetVMParam = @{
  ProcessorCount =  2
  DynamicMemory =  $True
  MemoryMinimumBytes =  1GB
  MemoryMaximumBytes =  3Gb
  ErrorAction =  'Stop'
  PassThru =  $True
  Verbose =  $True
  }

$VM = New-VM @NewVMParam 
$VM = $VM | Set-VM @SetVMParam 


Set-VMKeyProtector -VMName $VMN -NewLocalKeyProtector
Enable-VMTPM -VMName $VMN

##### Copy gold image
$dst="F:\VMs\$VMN\boot.vhdx"
Add-Content $logfile -Value "Copying Disk"
Copy-Item $bootIMG $dst
Add-Content $logfile -Value  "Starting $VMN"
Start-vm -Name $VMN
}


$logfile = "f:\APProvision.log"

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

####### Import Modules 
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
Install-Module Subnet -Force
Install-Module WindowsAutoPilotIntune -Force
Install-Module Microsoft.Graph.Intune -Force
Install-Module -Name 7Zip4Powershell -RequiredVersion 1.9.0 -Force
Add-Content $logfile -Value "Modules Imported"

###### Create Hyper-V infrastructure 
New-VMSwitch -Name "NestedSwitch" -SwitchType Internal

$NIC1IP = Get-NetIPAddress | Where-Object -Property AddressFamily -EQ IPv4 | Where-Object -Property IPAddress -EQ $NIC1IPAddress
$NIC2IP = Get-NetIPAddress | Where-Object -Property AddressFamily -EQ IPv4 | Where-Object -Property IPAddress -EQ $NIC2IPAddress

$NATSubnet = Get-Subnet -IP $NIC1IP.IPAddress -MaskBits $NIC1IP.PrefixLength
$HyperVSubnet = Get-Subnet -IP $NIC2IP.IPAddress -MaskBits $NIC2IP.PrefixLength
$NestedSubnet = Get-Subnet $GhostedSubnetPrefix
$VirtualNetwork = Get-Subnet $VirtualNetworkPrefix

New-NetIPAddress -IPAddress $NestedSubnet.HostAddresses[0] -PrefixLength $NestedSubnet.MaskBits -InterfaceAlias "vEthernet (NestedSwitch)"
New-NetNat -Name "NestedSwitch" -InternalIPInterfaceAddressPrefix "$GhostedSubnetPrefix"

Add-DhcpServerv4Scope -Name "Nested VMs" -StartRange $NestedSubnet.HostAddresses[1] -EndRange $NestedSubnet.HostAddresses[-1] -SubnetMask $NestedSubnet.SubnetMask
Set-DhcpServerv4OptionValue -DnsServer 168.63.129.16 -Router $NestedSubnet.HostAddresses[0]

Install-RemoteAccess -VpnType RoutingOnly
cmd.exe /c "netsh routing ip nat install"
cmd.exe /c "netsh routing ip nat add interface ""$($NIC1IP.InterfaceAlias)"""
cmd.exe /c "netsh routing ip add persistentroute dest=$($NatSubnet.NetworkAddress) mask=$($NATSubnet.SubnetMask) name=""$($NIC1IP.InterfaceAlias)"" nhop=$($NATSubnet.HostAddresses[0])"
cmd.exe /c "netsh routing ip add persistentroute dest=$($VirtualNetwork.NetworkAddress) mask=$($VirtualNetwork.SubnetMask) name=""$($NIC2IP.InterfaceAlias)"" nhop=$($HyperVSubnet.HostAddresses[0])"

Get-Disk | Where-Object -Property PartitionStyle -EQ "RAW" | Initialize-Disk -PartitionStyle GPT -PassThru | New-Volume -FileSystem NTFS -AllocationUnitSize 65536 -DriveLetter F -FriendlyName "Hyper-V"

Add-Content $logfile -Value "Hyper-V infra created"


####### Download and extract Gold image
########################################
$vmname = "Win10AutoPilot01"
$workfolder = "f:\VMs"
$imageFile ="f:\VMs\bootgold.7z"

New-Item -ItemType Directory -Path $workfolder -Force
Set-Location $workfolder
(New-Object System.Net.WebClient).DownloadFile($imageUrl, $imageFile)
Expand-7Zip -ArchiveFileName $imageFile -TargetPath $workfolder

######## Create autopilot nested VM
###################################
Create-VM -bootIMG "F:\VMs\boot-gold.vhdx" -VMN $vmname
Add-Content $logfile -Value "VM Created"


####### Import Modules 
######################
Import-Module -Name WindowsAutoPilotIntune -ErrorAction Stop
Import-Module -Name Microsoft.Graph.Intune 
$user="marco"
$pass ='@MegaP@$$W0rd!%&'
$secpassword = ConvertTo-SecureString $pass -AsPlainText -Force
$localcred = New-Object -TypeName System.Management.Automation.PSCredential ( $user, $secpassword)
$authority = "https://login.windows.net/$Tenant"

######## Connect to MS Graph
############################
Update-MSGraphEnvironment -AppId $ClientID -Quiet
Update-MSGraphEnvironment -AuthUrl $authority -Quiet
Connect-MSGraph -ClientSecret $ClientSecret -Quiet


####### Get a PS Session with the nested VM
###########################################

$session =$null
while ( $session -eq $null) {
    $session = New-PSSession -Credential $localcred -VMName $vmname -ErrorAction Ignore
    if ($session) { break}
    Add-Content $logfile -Value  "PS Session not ready. Retrying in 1 min "
    Start-Sleep -Seconds 60 
}


######## Get HWInfo from the nested VM
######################################

$d =Invoke-Command -Session $session -command  { c:\powershell\get-ap-info.ps1 }  
Add-AutoPilotImportedDevice  -serialNumber $d.'Device Serial Number' -hardwareIdentifier $d.'Hardware Hash' -orderIdentifier "VMs"
Start-Sleep 20

Add-Content $logfile -Value  $d.'Device Serial Number'

#### Import AutoPilot Device in Intune
######################################
$impdev = $null 
while ( $impdev -eq $null ){
try{
$impdev = ( Get-AutoPilotDevice  |?{$_.serialNumber -eq $d.'Device Serial Number'}) 
}
catch{}

if($impdev) 
{
 Add-Content $logfile -Value  "Device has been Imported"
 break
 }
Add-Content $logfile -Value  "Device has not been imported yet. Sleeping 2 min"
Invoke-AutopilotSync
Start-Sleep -Seconds 120 
}

#### Wait for AP profile to be assigned 
#######################################

$ProfileAssigned ="notAssigned"
while (($ProfileAssigned -like "notAssigned") -or ($ProfileAssigned -like "*pending*")){
    try{
        $ProfileAssigned =  (Get-AutoPilotDevice|?{$_.serialNumber -eq $d.'Device Serial Number'}).deploymentProfileAssignmentStatus
        }
    catch{
    }
    Add-Content $logfile -Value  "Profile has not been assigned yet. Sleeping 1 min"
    Start-Sleep -Seconds 60 
}

Add-Content $logfile -Value "Profile assigned"

### reset the nested autopilot device 
 Write-Output "Invoking remote wipe of $VMName"
Invoke-Command -Session $session -command  { c:\powershell\startwipe.bat } 

Deploy the template

Now we are ready to deploy the template

  • connect to azure portal
  • in the search box look for templates and select deploy a custom template
  • in the new window select Build your own template in the editor
  • in the new window click load file and select the azuredeploy.json form the cloned repository and click save
  • once done click review and create and deploy the template

The deployment will take approximate 30-40 minutes to complete. However, you will need to give it more time since the nested VM needs to reset before the environment is ready

Validate the deployment

Once the deployment is ready ( approximate 2 hours) go to the newly created resource group and select the Hyper-V host.

  • Connect to the host using RDP and the credentials you used when creating the deployment

When connected, open Hyper-V Manager and connect to the nested VM created there. If your tenant has been configured correctly you will get the OOBE screen

Enjoy your Autopilot testing