Generate NanoServer VHDX image easily with PowerShell

The last month I have presented how to generate a NanoServer VHDX image from the Windows Server 2016 Technical Preview 2 ISO. If you have read the topic, you have seen that there are a lot of steps. So I have decided to create a PowerShell script to generate NanoServer VHDX image and I want to share it in this topic.

This script enables me to save lot of time and avoid me to make mistake. There are some requirements to run this script. If you have never generated a VHDX from NanoServer.wim file, I recommend you to read this post. So to run my script you need:

  • An unattend.xml file ready;
  • A SetupComplet.cmd file ready;
  • The Windows Server 2016 TC2 ISO downloaded;
  • The Convert-WindowsImage.ps1 downloaded and copied in the same folder than my script (you can download this script here)

Unattend.xml file example

Bellow you can find an example of the Unattend.xml file. I use it to create my VHDX image:

<?xml version='1.0' encoding='utf-8'?>
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="https://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance">
    <settings pass="offlineServicing">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
            <ComputerName>NanoServer04</ComputerName>
        </component>
    </settings>

    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
            <UserAccounts>
                <AdministratorPassword>
                    <Value>password </Value>
                    <PlainText>true</PlainText>
                </AdministratorPassword>
            </UserAccounts>
            <TimeZone>Romance Standard Time</TimeZone>
        </component>
    </settings>

    <settings pass="specialize">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
            <RegisteredOwner>Romain Serre</RegisteredOwner>
            <RegisteredOrganization>Tech-Coffee</RegisteredOrganization>
        </component>
    </settings>
</unattend>

SetupComplete.cmd file example

You need also a SetupComplete.cmd file to run some commands on first boot. I use the below script in my lab:

netsh advfirewall set all state off
netsh interface ip set address "Ethernet" static 10.10.0.203 255.255.255.0 10.10.0.1
netsh interface ipv4 add dnsserver "Ethernet" address=10.10.0.5 index=1
hostname
ipconfig

Script to generate the NanoServer VHDX image

Below you can find the script that I have written. Copy the script in New-NSVHDX.ps1 file.

#requires -version 4.0
#requires –runasadministrator
 
<#
.SYNOPSYS
    This script enables to create Nano Server VHDX image from the NanoServer.wim file
.VERSION
    0.1: Initial version
    0.2: Change in the script to work with Convert-WindowsImage.ps1 Version 10
.AUTHOR
    Romain Serre
    Blog: //www.tech-coffee.net
    Twitter: @RomSerre
.PARAMETERS
    WorkFolder: Specify the work folder where will be stored WIM, VHDX, Dism and so on
    VHDXName: Specify the VHDX file name (without .vhdx)
    ISOLiteralPath: Specify the absolute path to the Windows Server 2016 ISO
    UnattendFile: Specify the absolute path to your unattend.xml file
    SetupCompleteFile: Specify the absolute path to SetupComplete.cmd file
.EXAMPLE
   New-NSVHDX -WorkFolder C:\temp\NanoPrep `
              -VHDXName NanoServer01 `
              -ISOLiteralPath "C:\Temp\10074.0.150424-1350.fbl_impressive_SERVER_OEMRET_X64FRE_EN-US.ISO" `
              -UnattendFile "c:\temp\unattend.xml" `
              -SetupCompleteFile "c:\temp\SetupComplete.cmd"
 
#>
 
##### Parameters #####
# ----------------------------------------------------------------
 
[CmdletBinding()]
param(
    [Parameter(Mandatory=$True, HelpMessage='Specify the work folder where will be stored WIM, VHDX, Dism and so on')]
    [Alias('WorkFolder')]
    [string]$ParentFolder,
    [Parameter(Mandatory=$True, HelpMessage='Specify the VHDX file name (without .vhdx)')]
    [Alias('VHDXName')]
    [String]$OutputImageName,
    [Parameter(Mandatory=$True, HelpMessage='Specify the absolute path to the Windows Server 2016 ISO')]
    [Alias('ISOLiteralPath')]
    [String]$ISOPath,
    [Parameter(Mandatory=$True, HelpMessage='Specify the absolute path to your unattend.xml file')]
    [Alias('UnattendFile')]
    [String]$UnattendFilePath,
    [Parameter(Mandatory=$True, HelpMessage='Specify the absolute path to SetupComplete.cmd file')]
    [Alias('SetupCompleteFile')]
    [String]$SetupCompleteFilePath
    )
 
##### Variables #####
# ----------------------------------------------------------------
 
$Vnb = "0.2"
 
# Convert-WindowsImage script name
$ConvertWindowsImageScript = "Convert-WindowsImage.ps1"

# Set SubFolder name
$DismFolder                = "Dism"
$VHDXFolder                = "VHDX"
$PkgFolder                 = "Packages"
$MountFolder               = "MountDir"
$WIMFolder                 = "WIM"
 
##### NanoServer Packages list #####.
# Comment packages that you don't want to add to the final VHDX
# ----------------------------------------------------------------
 
$PackagesList = @(
                "Microsoft-NanoServer-Compute-Package.cab",
                "Microsoft-NanoServer-OEM-Drivers-Package.cab",
                "Microsoft-NanoServer-FailoverCluster-Package.cab",
                "Microsoft-NanoServer-Guest-Package.cab",
                "Microsoft-NanoServer-Storage-Package.cab"
                )
 
##### Main Code (do not modify if you don't know what you do) #####
# -----------------------------------------------------------------
 
# SubFolders in the WorkSpace
$SubFolders = @(
                "\$DismFolder",
                "\$VHDXFolder",
                "\$PkgFolder",
                "\$MountFolder",
                "\$WIMFolder"
               )
 
# Get script path
$ScriptPath = Split-Path -Parent $PSCommandPath
$($ScriptPath + "\$ConvertWindowsImageScript")
Clear
Write-Host "  ################################################################" -ForeGroundColor Green
Write-Host "  #     Welcome to the Nano Server VHDX image generator $Vnb      #" -ForeGroundColor Green
Write-Host "  #         This script has been written by Romain Serre         #" -ForeGroundColor Green
Write-Host "  #                     Twitter: @RomSerre                       #" -ForeGroundColor Green
Write-Host "  #               Blog: //www.tech-coffee.net               #" -ForeGroundColor Green
Write-Host "  ################################################################" -ForeGroundColor Green
Write-Host
Write-Host
 
# Veryfing if Convert-WindowsImage.ps1 is present
if (!(Test-Path $($ScriptPath + "\$ConvertWindowsImageScript"))){
    Write-Host "$((Get-Date -Format g)) - Can't find $ConvertWindowsImageScript. Please copy $ConvertWindowsImageScript in the same directory that this script." -ForeGroundColor Red
    Exit
}

. $($ScriptPath + "\$ConvertWindowsImageScript") 
 
# Veryfing if ISO file exists
if (!(Test-Path $ISOPath)){
    Write-Host "$((Get-Date -Format g)) - Can't find $ISOPath. Please specify a valid path to the Windows Server 2016 ISO." -ForeGroundColor Red
    Exit
}
 
# Veryfing if unattend.xml file exists
if (!(Test-Path $UnattendFilePath)){
    Write-Host "$((Get-Date -Format g)) - Can't find $UnattendFilePath. Please specify a valid path to the unattend.xml file." -ForeGroundColor Red
    Exit
}
 
# Veryfing if SetupComplete.cmd file exists
if (!(Test-Path $SetupCompleteFilePath)){
    Write-Host "$((Get-Date -Format g)) - Can't find $SetupCompleteFilePath. Please specify a valid path to SetupComplete.cmd file." -ForeGroundColor Red
    Exit
}
 
if (!(Test-Path $ParentFolder)){
    Write-Host "$((Get-Date -Format g)) - Working folder doesn't exist. Creating working folder" -ForeGroundColor Cyan
    New-Item -ItemType Directory -Path $ParentFolder > $Null
}
Else {
    Write-Host "$((Get-Date -Format g)) - Working folder already exists. Skipping creation" -ForeGroundColor Cyan
}
 
# Preparing Subfolder in workspace
Write-Host "$((Get-Date -Format g)) - Creating subfolder in workspace ..." -ForeGroundColor Cyan
Foreach ($SubFolder in $SubFolders){
 
    if (!(Test-Path $($ParentFolder + $SubFolder))){
        Write-Host "$((Get-Date -Format g)) - Creating $($ParentFolder + $SubFolder) sub folder" -ForeGroundColor Cyan
        New-Item -ItemType Directory -Path $($ParentFolder + $SubFolder) > $null
    }
    Else {
        Write-Host "$((Get-Date -Format g)) - $($ParentFolder + $SubFolder) already exists. Skipping creation." -ForeGroundColor Cyan
    }
}
 
# Preparing NanoServer VHDX name
$OutputImageName = $OutputImageName + ".vhdx"
Write-Host "$((Get-Date -Format g)) - The VHDX image will be called $OutputImageName" -ForegroundColor Cyan
 
# Copying requirements from Windows Server 2016 ISO
Write-Host "$((Get-Date -Format g)) - Mounting $ISOPath ..." -ForeGroundColor Cyan
Mount-DiskImage $ISOPath
 
$DriveLetter = (Get-DiskImage $ISOPath | Get-Volume).DriveLetter
Write-Host "$((Get-Date -Format g)) - The ISO is mounted on $($DriveLetter):\" -ForegroundColor Cyan
 
$ISONanoPath   = $DriveLetter + ":\NanoServer"
$ISOSourcePath = $DriveLetter + ":\Sources"
 
Write-Host "$((Get-Date -Format g)) - Copying NanoServer.wim to $($ParentFolder + "\$WIMFolder")" -ForeGroundColor Cyan
Copy-Item -Path $($ISONanoPath + "\NanoServer.wim") -Destination $($ParentFolder + "\$WIMFolder") -For
 
Write-Host "$((Get-Date -Format g)) - Copying Packages folder to $($ParentFolder + "\$PkgFolder")" -ForeGroundColor Cyan
Copy-Item -Path $($ISONanoPath + "\Packages\*") -Destination $($ParentFolder + "\$PkgFolder") -Recurse -Force
 
Write-Host "$((Get-Date -Format g)) - Copying api*downlevel*.dll to $($ParentFolder + "\$DismFolder")" -ForeGroundColor Cyan
Copy-Item -Path $($ISOSourcePath + "\api*downlevel*.dll") -Destination $($ParentFolder + "\$DismFolder") -Force
 
Write-Host "$((Get-Date -Format g)) - Copying *dism* to $($ParentFolder + "\$DismFolder")" -ForeGroundColor Cyan
Copy-Item -Path $($ISOSourcePath + "\*dism*") -Destination $($ParentFolder + "\$DismFolder") -Force
 
Write-Host "$((Get-Date -Format g)) - Copying *provider* to $($ParentFolder + "\$DismFolder")" -ForeGroundColor Cyan
Copy-Item -Path $($ISOSourcePath + "\*provider*") -Destination $($ParentFolder + "\$DismFolder") -Force
 
Dismount-DiskImage $ISOPath
Write-Host "$((Get-Date -Format g)) - The ISO is dismounted" -ForegroundColor Cyan
 
# Converting WIM file to VHDX
Write-Host "$((Get-Date -Format g)) - Converting NanoServer.wim into VHDX for Gen2 Virtual Machines... " -ForegroundColor Cyan


Convert-WindowsImage -SourcePath $($ParentFolder + "\$WIMFolder\NanoServer.wim") `
                     -VHD $($ParentFolder + "\$VHDXFolder\$OutputImageName") `
                     -Edition "CORESYSTEMSERVER_INSTALL" > $Null
 
Write-Host "$((Get-Date -Format g)) - Mounting $($ParentFolder + "\$VHDXFolder\$($OutputImageName)") in $($ParentFolder + "\$MountFolder")" -ForegroundColor Cyan
cmd.exe /c "$ParentFolder\$DismFolder\dism.exe" /Mount-Image /ImageFile:$($ParentFolder + "\$VHDXFolder\$($OutputImageName)") /index:1 /MountDir:$($ParentFolder + "\$MountFolder") > $null
Foreach ($Package in $PackagesList){
 
    Write-Host "$((Get-Date -Format g)) - Adding $Package to the $($ParentFolder + "\$VHDXFolder\$($OutputImageName)")" -ForegroundColor Cyan
    cmd.exe /c "$ParentFolder\$DismFolder\dism.exe" /add-Package /PackagePath:$($ParentFolder + "\$PkgFolder\$Package") /Image:$($ParentFolder + "\$MountFolder") > $null
    cmd.exe /c "$ParentFolder\$DismFolder\dism.exe" /add-Package /PackagePath:$($ParentFolder + "\$PkgFolder\en-us\$Package") /Image:$($ParentFolder + "\$MountFolder") > $null
}
 
Write-Host "$((Get-Date -Format g)) - Committing change and unmounting image" -ForegroundColor Cyan
cmd.exe /c "$ParentFolder\$DismFolder\dism.exe" /Unmount-Image /Mountdir:$($ParentFolder + "\$MountFolder") /commit > $Null
 
# Applying the unattend.xml file to the image
Write-Host "$((Get-Date -Format g)) - Mounting $($ParentFolder + "\$VHDXFolder\$($OutputImageName)") in $($ParentFolder + "\$MountFolder")" -ForegroundColor Cyan
cmd.exe /c "$ParentFolder\$DismFolder\dism.exe" /Mount-Image /ImageFile:$($ParentFolder + "\$VHDXFolder\$($OutputImageName)") /index:1 /MountDir:$($ParentFolder + "\$MountFolder") > $null
 
Write-Host "$((Get-Date -Format g)) - Applying $UnattendFilePath" -ForegroundColor Cyan
cmd.exe /c "$ParentFolder\$DismFolder\dism.exe" /image:$($ParentFolder + "\$MountFolder") /Apply-Unattend:$UnattendFilePath > $null
 
Write-Host "$((Get-Date -Format g)) - Creating $($ParentFolder + "\$MountFolder\windows\panther")" -ForegroundColor Cyan
New-Item -Type Directory -Path $($ParentFolder + "\$MountFolder\windows\panther") > $null
 
Write-Host "$((Get-Date -Format g)) - Copying $UnattendFilePath to $($ParentFolder + "\$MountFolder\windows\panther")" -ForegroundColor Cyan
Copy-Item $UnattendFilePath $($ParentFolder + "\$MountFolder\windows\panther")
 
Write-Host "$((Get-Date -Format g)) - Committing change and unmounting image" -ForegroundColor Cyan
cmd.exe /c "$ParentFolder\$DismFolder\dism.exe" /Unmount-Image /Mountdir:$($ParentFolder + "\$MountFolder") /commit > $null
 
# Copy the SetupComplete.cmd file to the image
Write-Host "$((Get-Date -Format g)) - Mounting $($ParentFolder + "\$VHDXFolder\$($OutputImageName)") in $($ParentFolder + "\$MountFolder")" -ForegroundColor Cyan
cmd.exe /c "$ParentFolder\$DismFolder\dism.exe" /Mount-Image /ImageFile:$($ParentFolder + "\$VHDXFolder\$($OutputImageName)") /index:1 /MountDir:$($ParentFolder + "\$MountFolder") > $null
 
Write-Host "$((Get-Date -Format g)) - Creating $($ParentFolder + "\$MountFolder\windows\Setup\Scripts")" -ForegroundColor Cyan
New-Item -Type Directory -Path $($ParentFolder + "\$MountFolder\windows\Setup\Scripts") > $null
 
Write-Host "$((Get-Date -Format g)) - Copying $SetupCompleteFilePath to $($ParentFolder + "\$MountFolder\windows\Setup\Scripts")" -ForegroundColor Cyan
Copy-Item $SetupCompleteFilePath $($ParentFolder + "\$MountFolder\windows\Setup\Scripts")
 
Write-Host "$((Get-Date -Format g)) - Committing change and unmounting image" -ForegroundColor Cyan
cmd.exe /c "$ParentFolder\$DismFolder\dism.exe" /Unmount-Image /Mountdir:$($ParentFolder + "\$MountFolder") /commit > $null
 
Write-Host "Your NanoServer VHDX image is ready in $($ParentFolder + "\$VHDXFolder\$($OutputImageName)")" -ForegroundColor Cyan
Write-Host
Write-Host
Write-Host "Thank you to have used this script. Please if you have got some issues, make a feedback :)" -ForegroundColor Green

Use the script to generate the image

First copy the script and Convert-WindowsImage.ps1 in the same folder.

If you want manage the packages that will be added to the image, you can edit the script and modify the $PackagesList array to remove or to add packages (around line 69).

Then open a PowerShell command in RunAs Administrator mode and run the below command. Make sure that your ExecutionPolicy is set to RemoteSigned or Unrestricted by using the cmdlet Get-ExecutionPolicy. You can set the execution policy by using Set-ExecutionPolicy cmdlet.

.\New-NSVHDX -WorkFolder C:\temp\NanoFolder `
             -VHDXName NanoServer04 `
             -ISOLiteralPath "C:\Temp\10074.0.150424-1350.fbl_impressive_SERVER_OEMRET_X64FRE_EN-US.ISO" `
             -UnattendFile "c:\temp\NanoPrep\unattend.xml" `
             -SetupCompleteFile "c:\temp\NanoPrep\SetupComplete.cmd"

Below the explanation of each parameter:

  • WorkFolder: this is the folder where Dism, VHDX, WIM and packages will be stored;
  • VHDXName: this is the name of the final VHDX file;
  • ISOLiteralPath: specify the absolute path to the Windows Server 2016 TC2 ISO;
  • UnattendFile: specify the absolute path to the unattend.xml;
  • SetupCompleteFile: Specify the absolute path to the SetupComplete.cmd.

When the script is finished you should have something like that:

Verifying if the VHDX image works

Now that the VHDX is generated, you can create a virtual machine and attach the VHDX:

Next I start the Virtual Machine and the commands added to the SetupComplete.cmd should run on first boot:

Great it’s working J. So now I’m trying to connect to the server with PowerShell:

It’s good I can connect to my NanoServer04 J.

About Romain Serre

Romain Serre works in Lyon as a Senior Consultant. He is focused on Microsoft Technology, especially on Hyper-V, System Center, Storage, networking and Cloud OS technology as Microsoft Azure or Azure Stack. He is a MVP and he is certified Microsoft Certified Solution Expert (MCSE Server Infrastructure & Private Cloud), on Hyper-V and on Microsoft Azure (Implementing a Microsoft Azure Solution).

13 comments

  1. Warren Ridings

    Bonjour Romain,

    Many thanks for this blog post, I’ve successfully run through the script and generated the VHDX file. When I attempt to boot the VM I get prompted for a Bitlocker Key! I’m running client hyper-v on win10 build 10130 was this my mistake 😉

    Kind Regards,

    Warren

  2. Hey Romain,

    Really thankful for this blog post been looking for a tutorial like this for a while, I tried to do this but, for some reason when I run the New-NSVHDX.ps1 script, it gets to converting nanoserver.wim into VHDX for Gen2 Virtual Machines and then it hangs, Do you think you could help with this?

    • Whoops, I figured it out, I download a new copy of the Convert-WindowsImage.ps1 and it has an issue on line 4092 and I forgot to fix it, but I did and now it works perfectly, Thanks Again!

  3. Okay so different issue now, I run the script to make the VHDX it runs almost perfectly, it says everything works and that the VHDX has been made but when I go to the folder where it is suppose to be, there is nothing. I search my whole computer nothing, so it says it makes it. But it doesn’t actually do that. Any idea what the issue could be?

    • Hi Peter,

      Did you run the script in RunAs Administrator ? Could you verify if the WIM has been copied in the WIM folder as well as packages files, dism and so on ?

      I have not tested yet the new convert-WIndowsImage with my script. To verify if it’s working you can remove the “> $Null” at line 185.

      Thank you Peter.

      Romain.

      • Yeah I made sure to run the script as an Administrator, and yeah I checked everything is copied into its respective folder underneath the Parent Folder. I did remove the “> $Null” at line 185 and it does the same thing, everything seems like it works and says the VHDX is created but when i go and check in the folder there is no VHDX.

        • Hi Peter,

          I have found the problem. It is because COnvert-WindowsImage has been updated to version 10. I modify my script to work with the new version of Convert-WindowsImage and I post again.

          Thank you for your feedback 🙂

        • Hi Peter,

          I have updated the code of New-NSVHDX.ps1. Could you try it ? (I suggest you to copy all and past it)

          Thank you peter.

          Romain.

  4. It worked perfectly!! Thank you so much for the help

  5. Just a small feedback : execute Set-ExecutionPolicy with unrestricted or remote before trying to run your script (because it’s not signed)
    https://technet.microsoft.com/library/hh847748.aspx

    • Hi Stan,

      I have modified this blog post to take into consideration the PowerShell execution policy.

      Thank you 🙂

      Romain.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

x

Check Also

Introduction to System Insights

It’s been a while that Microsoft releases new Windows Server 2019 builds regularly. The latest ...

Dell R730XD bluescreen with S130 adapter and S2D

This week I worked for a customer which had issue with his Storage Spaces Direct ...

Windows Server 2016 TP5: Bugcheck 0xc000021A

If you have implemented a lab (I hope so not in production J) with some ...