Customizing a Generation 2 VHDX

Table of contents

In a previous article I demonstrated how to create a VHDX file that you could use when creating a Generation 2 Hyper-V virtual machine. If you recall, part of the process included creating partitions for recovery information. However, I didn’t do anything with them at the time so let me guide you on how to use these partitions and even speed up deployment of the new virtual machine. Many of these commands have command line counterparts but we will use PowerShell 4.0. This process will also require the Storage and DISM cmdlets. I will walk through the process with these items:

$path = "D:\vhd\demo3.vhdx
$WIMPath ="D:\wim\Win2012R2-Install.wim"
$Index = 2

How did I know I wanted an index of 2? By looking at what is inside the WIM file with the Get-WindowsImage cmdlet.

Part of my process will install Windows Server 2012 R2 Standard (GUI). Let’s begin.

WARNING: You will be using commands that involve partitioning and boot configurations. Don’t try anything I am going to demonstrate on a production system.

First, I need to mount the VHDX file and get the disk number

Mount-DiskImage -ImagePath $Path
$disknumber = (Get-DiskImage -ImagePath $path | Get-Disk).Number

Next, I want to prepare the recovery image partition by formatting it and assigning it a drive letter if it doesn’t already have one.

Get-Partition -DiskNumber $disknumber -PartitionNumber 5 | Format-Volume -FileSystem NTFS -NewFileSystemLabel "RecoveryImage" -confirm:$false

if (-Not (Get-Partition -DiskNumber $disknumber -PartitionNumber 5).DriveLetter) {
Get-Partition -DiskNumber $disknumber -PartitionNumber 5 | Add-PartitionAccessPath -AssignDriveLetter
}

I am adding a drive letter so that I can create a folder and copy the WIM file. This is the recovery image. In the event of a serious problem, you could reapply the image using DISM from the recovery console or the Refresh PC settings under advanced tools when in repair mode. Of course, your best protection are adequate operating system and Hyper-V backups. The recovery image can be your last resort option.

In order for this to work, at least in my development, I had to create a folder in the Recovery Image partition.

$recoveryPartition = get-partition -DiskNumber $disknumber -PartitionNumber 5
$recoverfolder = Join-path "$($recoveryPartition.DriveLetter):" "Recovery"
mkdir $recoverFolder
$recoveryPath = Join-Path $recoverfolder "install.wim"

And then copy the WIM to that path as Install.wim.

Copy-Item -Path $WIMPath -Destination $recoverypath

Next, I’m going to go ahead apply that image directly to the operating system partition, in effect installing Windows Server 2012. This technique seems to be much faster than a traditional interactive install. Of course, this will require that the operating system partition have a drive letter.

Get-Partition -DiskNumber $disknumber -PartitionNumber 4 | Format-Volume -FileSystem NTFS -NewFileSystemLabel "Windows" -confirm:$false

if (-Not (Get-Partition -DiskNumber $disknumber -PartitionNumber 4).DriveLetter) {
Get-Partition -DiskNumber $disknumber -PartitionNumber 4 | Add-PartitionAccessPath -AssignDriveLetter
}

With a drive letter, I can now apply the image using the DISM cmdlets.

$windowsPartition = Get-Partition -DiskNumber $disknumber -PartitionNumber 4
$WinPath = Join-Path "$($windowsPartition.DriveLetter):" "\"
$windir = Join-path $winpath Windows
Expand-WindowsImage -ImagePath $recoveryPath -Index $Index -ApplyPath $WinPath

Next, I want to make sure a copy of the Windows recovery tools are in the right partition. Since this is a UEFI disk, they need to go in a separate partition, which I just happen to have. As with the other partitions, I’m quickly reformatting to make sure I have a clean slate and assigning a drive letter.

Get-Partition -DiskNumber $disknumber -PartitionNumber 1 | Format-Volume -FileSystem NTFS -NewFileSystemLabel "Windows RE Tools" -confirm:$false

if (-Not (Get-Partition -DiskNumber $disknumber -PartitionNumber 1).DriveLetter) {
Get-Partition -DiskNumber $disknumber -PartitionNumber 1 | Add-PartitionAccessPath -AssignDriveLetter
}
$retools = Get-Partition -disknumber $disknumber -partitionNumber 1

The recovery tools must go in a specific folder which I can create.

$repath = mkdir "$($retools.driveletter):\Recovery\WindowsRE"

The recovery tools wim (winre.wim) can be found inside the primary WIM file under \Windows\System32\Recovery but it is a hidden file so I have to tell PowerShell to look for hidden files and then copy it to the recovery tools partition.

dir "$($windowsPartition.DriveLetter):\Windows\System32\recovery\winre.wim" -hidden | Copy -Destination $repath.FullName

I will need to make sure the disk will boot properly, i.e. UEFI, so I need to get the System partition and assign it a drive.

if (-Not (Get-Partition -DiskNumber $disknumber -PartitionNumber 2).DriveLetter) {
Get-Partition -DiskNumber $disknumber -PartitionNumber 2 | Add-PartitionAccessPath -AssignDriveLetter
}
$systemPartition = Get-Partition -DiskNumber $disknumber -PartitionNumber 2
$sysDrive = "$($systemPartition.driveletter):"

Because with this information I can re-run BCDBoot to configure all the necessary boot information.

$cmd = "$windir\System32\bcdboot.exe $windir /s $sysDrive /F UEFI"
Invoke-Expression $cmd

The last step is to make sure recovery is properly configured using the Reagentc.exe command line tool. I will use this expression

$cmd = "$windir\System32\reagentc.exe /setosimage /path $recoverfolder /index $index /target $windir"
Invoke-Expression $cmd

Which becomes this: H:\Windows\System32\reagentc.exe /setosimage /path G:\Recovery /index 1 /target H:\Windows. I need to warn you about an quirk with this command in PowerShell. You will most likely see a message in red text that looks like an error, well, technically as far as PowerShell is concerned it is. However, you should also see message text that the operation was successful, which it will be. I don’t know why PowerShell thinks there is an error. But as long as I see “success” I ignore the error. At this point the partitions are configured. Personally, I like cleaning up the drive letters.

get-partition -DiskNumber $disknumber | where {$_.driveletter} | foreach {
$dl = "$($_.DriveLetter):"
$_ | Remove-PartitionAccessPath -accesspath $dl
}

All that remains is to dismount the disk.

Dismount-DiskImage -ImagePath $path

The VHDX file is now ready to use with your Gen2 Hyper-V virtual machine! But wait, it gets better. Since we are already applying an image, we can go ahead and stick in an answer file to complete the installation. The file must be called unattend.xml and it will go in the root of the operating system partition. Creating a complete XML file is beyond the scope of this article, but here is a bare bones one that I use to configure the default administrator password, computer name and time zone.

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:ms="urn:schemas-microsoft-com:asm.v3" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<settings pass="oobeSystem">
    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <UserAccounts>
           <AdministratorPassword>
              <Value>P@ssw0rd</Value>
              <PlainText>true</PlainText>
           </AdministratorPassword>
        </UserAccounts>
    </component>
</settings>
<settings pass="specialize">
    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <ComputerName>R2Core-Base</ComputerName>
        <RegisteredOwner>Administrator</RegisteredOwner>
        <RegisteredOrganization>Globomantics</RegisteredOrganization>
        <TimeZone>Eastern Standard Time</TimeZone>
    </component>
</settings>
</unattend>

Remember back in the process where I expanded the WIM file and applied it to the operating system partition? Well it is just another simple step to copy an unattend XML file to the root of the OS Partition. The source XML file can be called anything you want. Just make sure you copy it as Unattend.xml.

$unattendpath = Join-Path $winpath "Unattend.xml"
Copy-item c:\work\myunattend.xml -Destination $unattendpath

Continue on with the rest of the process, create the virtual machine and when it starts you will have a configured server ready to go. At least configured as much you set in your unattend.xml file.

Now, I certainly don’t expect you to type everything I’ve demonstrated. Instead I will give you a function that will handle everything for you automatically, including copying the unattend.xml file, if you specify one. The assumption is that you will be configuring a Gen 2 virtual disk that you created with my New-Gen2Disk function from my previous article.

#requires -version 4.0
#requires -modules DISM,Storage
#requires -RunAsAdministrator

Function Set-Gen2Partition {

<#
.SYNOPSIS
Configure Windows image and recovery partitions
.DESCRIPTION
This command will update partitions for a Generate 2 VHDX file, configured for
UEFI. It is assumed you used the New-Gen2Disk to create the VHDX file and that
the partitions are in this order

  1 = Recovery Tools
  2 = System                       
  3 = Reserved (MSR)                     
  4 = Basic (Windows)                       
  5 = Recovery Image                    

You must supply the path to the VHDX file and a valid WIM. You should also
include the index number for the Windows Edition to install. The WIM will be
copied to the recovery partition.

Optionally, you can also specify an XML file to be inserted into the OS
partition as unattend.xml

CAUTION: This command will reformat partitions.

.EXAMPLE
PS C:\> Set-Gen2Partition -Path D:\vhd\demo3.vhdx -WIMPath D:\wim\Win2012R2-Install.wim -verbose

VERBOSE: Processing D:\vhd\demo3.vhdx
VERBOSE: 

   Disk Number: 3

PartitionNumber  DriveLetter Offset                               Size Type                         
---------------  ----------- ------                               ---- ----                         
1                            1048576                            300 MB Recovery                     
2                            315621376                          100 MB System                       
3                            420478976                          128 MB Reserved                     
4                            554696704                        14.48 GB Basic                        
5                            16107175936                         15 GB Recovery                     

VERBOSE: Processing disknumber 3
VERBOSE: Formatting Recovery Image
VERBOSE: Assigning drive letter to Recovery Image partition
VERBOSE: copying D:\wim\Win2012R2-Install.wim to G:\Recovery\install.wim
VERBOSE: Formatting Windows partition
VERBOSE: Assigning drive letter to Windows partition
VERBOSE: Applying image from G:\Recovery\install.wim to H:\ using Index 1
VERBOSE: Dism PowerShell Cmdlets Version 6.3.0.0

LogPath : C:\windows\Logs\DISM\dism.log

VERBOSE: Formatting Windows RE Tools partition
VERBOSE: Assigning drive letter to Windows RE Tools partition
VERBOSE: Creating Recovery\WindowsRE folder
VERBOSE: Copying H:\Windows\System32\recovery\winre.wim to J:\Recovery\WindowsRE
VERBOSE: Assigning drive letter to System partition
VERBOSE: Running bcdboot-> H:\Windows /s K: /f UEFI
VERBOSE: H:\Windows\System32\reagentc.exe /setosimage /path G:\Recovery /index 1 /target H:\Windows
Directory set to: \\?\GLOBALROOT\device\harddisk3\partition5\Recovery

H:\Windows\System32\reagentc.exe : REAGENTC.EXE: Operation Successful.
At line:1 char:1
+ H:\Windows\System32\reagentc.exe /setosimage /path G:\Recovery /index 1 /target  ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (REAGENTC.EXE: Operation Successful.:String) [], Remote 
   Exception
    + FullyQualifiedErrorId : NativeCommandError

VERBOSE: 

   Disk Number: 3

PartitionNumber  DriveLetter Offset                               Size Type                         
---------------  ----------- ------                               ---- ----                         
1                J           1048576                            300 MB Recovery                     
2                K           315621376                          100 MB System                       
3                            420478976                          128 MB Reserved                     
4                H           554696704                        14.48 GB Basic                        
5                G           16107175936                         15 GB Recovery                     

VERBOSE: Removing access paths
VERBOSE: Dismounting D:\vhd\demo3.vhdx
VERBOSE: Finished

.EXAMPLE
PS C:\> Set-Gen2Partition -Path D:\vhd\test3.vhdx -WIMPath D:\wim\Win2012R2-Install.wim -Unattend C:\scripts\unattend.xml

#>

[cmdletbinding(ConfirmImpact="High")]
Param(
[parameter(Position=0,Mandatory=$True,
HelpMessage="Enter the path to the VHDX file",
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
[Alias("FullName","pspath")]
[ValidateScript({Test-Path $_})]
[string]$Path,
[parameter(Position=1,Mandatory=$True,
HelpMessage="Enter the path to the WIM file")]
[ValidateScript({Test-Path $_})]
[string]$WIMPath,
[ValidateScript({, 
 $last = (get-windowsimage -ImagePath $PSBoundParameters.WIMPath | 
         sort ImageIndex | select -last 1).ImageIndex
 If ($_ -gt $last -OR $_ -lt 1) {
    Throw "enter a valid index between 1 and $last"
 }
 else {
    #index is valid
    $True
 }
})]
[int]$Index = 1,
[ValidateScript({Test-Path $_})]
[string]$Unattend
)

Process {
Write-Verbose "Processing $path"

if ($PSCmdlet.ShouldContinue("Are you sure you want to process $path`? Any existing data will be lost!","WARNING!")) {

    #mount the VHDX file
    Mount-DiskImage -ImagePath $Path

    #get the disk number
    $disknumber = (Get-DiskImage -ImagePath $path | Get-Disk).Number

    #pre-processing
    Write-Verbose (Get-Partition -DiskNumber $disknumber | out-string) 

    #prepare Recovery Image partition
    Write-Verbose "Processing disknumber $disknumber"

    Write-Verbose "Formatting Recovery Image"
    Get-Partition -DiskNumber $disknumber -PartitionNumber 5   |
    Format-Volume -FileSystem NTFS -NewFileSystemLabel "RecoveryImage" -confirm:$false |
    Out-Null

    #mount the Recovery image partition with a drive letter
    if (-Not (Get-Partition -DiskNumber $disknumber -PartitionNumber 5).DriveLetter) {
     Write-Verbose "Assigning drive letter to Recovery Image partition"
     Get-Partition -DiskNumber $disknumber -PartitionNumber 5 |
     Add-PartitionAccessPath -AssignDriveLetter
    }

    $recoveryPartition = get-partition -DiskNumber $disknumber -PartitionNumber 5  

    #copy the WIM to recovery image partition as Install.wim
    $recoverfolder = Join-path "$($recoveryPartition.DriveLetter):" "Recovery"

    mkdir $recoverFolder | Out-Null
    $recoveryPath = Join-Path $recoverfolder "install.wim"

    Write-Verbose "copying $WIMpath to $recoverypath"
    Copy-Item -Path $WIMPath -Destination $recoverypath 

    #mount the OS partition with a drive letter
    Write-Verbose "Formatting Windows partition"
    Get-Partition -DiskNumber $disknumber -PartitionNumber 4 |
    Format-Volume -FileSystem NTFS -NewFileSystemLabel "Windows" -confirm:$false |
    Out-Null

    if (-Not (Get-Partition -DiskNumber $disknumber -PartitionNumber 4).DriveLetter) {
    Write-Verbose "Assigning drive letter to Windows partition"
    Get-Partition -DiskNumber $disknumber -PartitionNumber 4 |
    Add-PartitionAccessPath -AssignDriveLetter
    }

    $windowsPartition = Get-Partition -DiskNumber $disknumber -PartitionNumber 4

    #apply the image from recovery to the OS partition
    $WinPath = Join-Path "$($windowsPartition.DriveLetter):" "\"
    $windir = Join-path $winpath Windows

    Write-Verbose "Applying image from $recoveryPath to $winpath using Index $index"
    Expand-WindowsImage -ImagePath $recoveryPath -Index $Index -ApplyPath $WinPath

    #copy XML file if specified 
    if ($Unattend) {
        $unattendpath = Join-Path $winpath "Unattend.xml"
        Write-Verbose "Copying $unattend to $unattendpath"
        Copy-item $Unattend -Destination $unattendpath 
    }

    #mount the recovery tools partition with a drive letter  
    Write-Verbose "Formatting Windows RE Tools partition"
    Get-Partition -DiskNumber $disknumber -PartitionNumber 1 |
    Format-Volume -FileSystem NTFS -NewFileSystemLabel "Windows RE Tools" -confirm:$false |
    Out-Null

    if (-Not (Get-Partition -DiskNumber $disknumber -PartitionNumber 1).DriveLetter) {
      Write-Verbose "Assigning drive letter to Windows RE Tools partition"
      Get-Partition -DiskNumber $disknumber -PartitionNumber 1 |
      Add-PartitionAccessPath -AssignDriveLetter
    }

    $retools =  Get-Partition -disknumber $disknumber -partitionNumber 1

    #create \Recovery\WindowsRE
    Write-Verbose "Creating Recovery\WindowsRE folder"
    $repath = mkdir "$($retools.driveletter):\Recovery\WindowsRE"

    Write-Verbose "Copying $($windowsPartition.DriveLetter):\Windows\System32\recovery\winre.wim to $($repath.fullname)"

    #the winre.wim file is hidden
    dir "$($windowsPartition.DriveLetter):\Windows\System32\recovery\winre.wim" -hidden |
    Copy -Destination $repath.FullName 

    #assign a letter to the System partition  
    Write-Verbose "Assigning drive letter to System partition"
    Get-Partition -DiskNumber $disknumber -PartitionNumber 2 |
    Add-PartitionAccessPath -AssignDriveLetter

    $systemPartition = Get-Partition -DiskNumber $disknumber -PartitionNumber 2 
    $sysDrive = "$($systemPartition.driveletter):"
    Write-Verbose "Running bcdboot-> $windir /s $sysDrive /f UEFI"
    $cmd = "$windir\System32\bcdboot.exe $windir /s $sysDrive /F UEFI"
    Invoke-Expression $cmd

   $cmd = "$windir\System32\reagentc.exe /setosimage /path $recoverfolder /index $index /target $windir"
    Write-Verbose $cmd
    Invoke-Expression $cmd

    #this doesn't appear to be necessary. I get a message this is already enabled
    # $cmd = "$windir\System32\reagentc.exe /setreimage /path $($repath.fullname) /target $windir"
    # Write-Verbose $cmd
    # invoke-expression $cmd

    #post processing
    Write-Verbose (Get-Partition -DiskNumber $disknumber | out-string) 

    #clean up
    Write-Verbose "Removing access paths"
    get-partition -DiskNumber $disknumber | where {$_.driveletter}  | foreach {
     $dl = "$($_.DriveLetter):"
     $_ | Remove-PartitionAccessPath -accesspath $dl
    }

    #dismount
    Write-Verbose "Dismounting $path"
    Dismount-DiskImage -ImagePath $path

    Write-Verbose "Finished"

} #confirm
else {
  Write-Verbose "Process aborted."
}

} #process

} #end function

When you run the function, you will be prompted if you want to continue because the disk partitions will be reformatted. Probably unnecessary if you are using a disk you just created but I wanted to accommodate disks created elsewhere. Be aware that the function will assume your partitions are in this order:

1 = Recovery Tools

2 = System

3 = Reserved (MSR)

4 = Basic (Windows)

5 = Recovery Image

How will all of this work? Here’s a simple PowerShell script to create a ready-to-go Windows Server 2012 R2 virtual machine. Like all of the commands I’ve been demonstrating this requires that you run as administrator in an elevated session.

#requires -version 4.0
#requires -modules DISM,Storage,Hyper-V
#requires -RunAsAdministrator

#demonstrate full provisioning of a new Gen2 HyperV VM

[cmdletbinding()]
Param()
Write-Host "$(get-date) Starting process" -ForegroundColor Green

#dot source functions. Adjust your paths accordingly 
#or paste the functions into this script
. C:\scripts\New-Gen2VHD.ps1
. C:\scripts\Set-Gen2Partition.ps1

#create the disk
$diskParams = @{
Path = "D:\VHD\DemoGen2VM.vhdx"
Size = 50GB
Dynamic = $True
ErrorAction = "Stop"
}

#parameters for partition command
$partParams = @{
WimPath = "D:\wim\Win2012R2-Install.wim"
Index = 2
Unattend = "C:\scripts\unattend-2012R2.xml"
ErrorAction = "Stop"
}

#create the disk and pipe to the partitioning command
Write-Host "$(get-date) Creating and partitioning the disk" -ForegroundColor Green
Try {
 New-Gen2Disk @diskParams #| Set-Gen2Partition @partParams
}
Catch {
 Write-warning "Ooops. $($_.exception.message)"
 #bail out
 Write-Host "$(get-date) aborting the script" -ForegroundColor red
 Return
}

#create the VM
$vmParams = @{
Name = "Demo Gen 2" 
Generation = 2 
VHDPath = $diskParams.path 
MemoryStartupBytes = 1GB
SwitchName = "Work Network"
}

#modify VM parameters
$setParams = @{
ProcessorCount = 2
DynamicMemory =$True
MemoryMaximumBytes = 2GB
Notes = "demonstration Gen 2 VM"
Passthru = $True

}

#create the VM and start it
Write-Host "$(get-date) Creating, modifying and starting the VM" -ForegroundColor Green

New-VM @vmParams | Set-VM @setParams | Start-VM

Write-Host "$(get-date) Ending process" -ForegroundColor Green

As you can see in the screen shot, it only takes a little under 5 minutes to create a complete Windows Server 2012 R2 standard server virtual machine running as a Generation 2 virtual machine.

You are limited to what operating systems are supported as Gen 2 but from a management perspective, Windows Server 2012 R2 is pretty fantastic so why not use it. Finally, I hope it goes without saying, but please test and re-test everything I have shown you in the last few articles in a non-production environment.

 

 

Altaro Hyper-V Backup
Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

35 thoughts on "Customizing a Generation 2 VHDX"

Leave a comment or ask a question

Your email address will not be published. Required fields are marked *

Your email address will not be published. Required fields are marked *

Notify me of follow-up replies via email

Yes, I would like to receive new blog posts by email

What is the color of grass?

Please note: If you’re not already a member on the Dojo Forums you will create a new account and receive an activation email.