Creating a Customizable Windows OVF Template

Share this:

Some time ago I published a post on creating Linux OVF template, apparently people liked the idea because I was asked to do same for a Windows 2016 Server image.

If you are a Windows guy, you for sure know that there are millions of ways to deploy customised Windows OS. Some are better some are worse, and the method I will describe here today is not the best either, but it works for it’s purpose.

To summarise: We have a Windows OS with preinstalled set of Software, and we want to pack it into a template for future redistribution.

We have the following requirements:

  • It should be possible to provide basic network configuration details as part of OVF deployment wizard.
  • System should be deployed with newly generated SID, so it can be joined to an Active Directory Domain.

Sounds simple enough, but the problem for me is that I am not that familiar with Windows, especially programming anything with it. Well, I always wanted to learn some Powershell, so this was a good opportunity to start reading.

So, to get going we need several things. Here is my task list:

Obviously we will need to create a VM with Windows inside. Also at this stage we can install all the Software needed.

Next, we need to enable vApp Options on the VM and create some properties. Process is exactly the same as in my Linux OVF article. Here is what I created on mine.

With the automation we need to solve the following issues:

  • Fetch vApp settings from inside the OS
  • Take those settings and configure whatever is needed
  • Execute Sysprep to regenerate the SID

Lets go one by one.

Whole Process

1. Considerations

Before we will do any development we need to do some considerations.

First thing we will need to decide is where to place our scripts. The path should be accessible by system, and Administrator user. It also need to stay in place after Sysprep is executed. I chose to place the scripts in C:\Program Files\vmenv\ .

Second thing is to create a template  XML for Sysprep unattended execution. It will be modified and applied by our script. I came up with very minimal Sysprep XML file which I will use here. I will attach it at the end for anyone who wants to reuse.

2. Fetch vApp Settings

To fetch vApp settings I will use VMware tools. Commands to do that is pretty similar to the one in Linux. I will fetch the data and write it to an XML file.

"C:\Program Files\VMware\VMware Tools\vmtoolsd.exe" --cmd="info-get guestinfo.ovfEnv" > "C:\Program Files\vmenv\vmenv.xml"

Actually I will make this line a separate getvmenv.bat script, for potential reuse by other apps in future.

3. Parse the data and assign variables

Once I have the vmenv.xml file I can fetch all the data I need from it and assign it to variables. Here is how I did it:

$vmenvxml = 'C:\Program Files\vmenv\vmenv.xml'    
[xml]$vmenv = Get-Content $vmenvxml
    # Collect Variables from vmenv XML
    $vmIP = $vmenv.Environment.PropertySection.Property | ?{ $_.key -like '*_IP*' } | select -expand value
    $vmNetmask = $vmenv.Environment.PropertySection.Property | ?{ $_.key -like '*Netmask*' } | select -expand value
    $vmGW = $vmenv.Environment.PropertySection.Property | ?{ $_.key -like '*Gateway*' } | select -expand value
    $vmHostname = $vmenv.Environment.PropertySection.Property | ?{ $_.key -like '*Hostname*' } | select -expand value
    $vmDNS = $vmenv.Environment.PropertySection.Property | ?{ $_.key -like '*DNS*' } | select -expand value

4. Apply Network settings

Now, we have variables with the settings we need, so we can now configure the network. First lets identify the name of Network connection we use on the system.

$ifname = Get-NetAdapter | Select -expand Name

Then, lets configure settings.

# Configure network interface
New-NetIPAddress –InterfaceAlias $ifname –IPAddress $vmIP –PrefixLength $vmNetmask -DefaultGateway $vmGW
# Configure DNS servers
Set-DnsClientServerAddress -InterfaceAlias $ifname -ServerAddresses $vmDNS

Note: This will work only in case you interface is not configured. If the interface has an IP configured, this code will fail. This is not an issue during new deployments, but keep this in mind while testing.

5. Configure Hostname

We will configure hostname as part of Sysprep, so what we need to do is to parse the Sysprep template XML I have created, replace hostname value and write the change to new Sysprep XML file. Here is how I did it:

$sysprepnofile = 'C:\Program Files\vmenv\unattend_no.xml'
$sysprepokfile = 'C:\Program Files\vmenv\unattend_ok.xml'
[xml]$vmsysprepenv = Get-Content $sysprepnofile

# Replace Hostname in Sysprep file and write it to disk
# If you have added nodes to sysprep, make sure array element number is fixed.
$vmsysprepenv.unattend.settings.Component[2].Computername = "$vmHostname"
$vmsysprepenv.Save($sysprepokfile)

6. Execute Sysprep

Now, once we have all we need we can execute Sysprep and let the VM reboot.

$sysprepexe = "C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /reboot /unattend:'$sysprepokfile'"
Invoke-Expression -Command $sysprepexe

7. Taking care of the state

We will be scheduling this script to run on boot, and we want to make sure that it runs only once on first boot, so we need to check run state. I implemented this logic with simple state file and if statement.

$firstbootstate = 'C:\Program Files\vmenv\firstboot.state'
($vmenvstate = Get-ChildItem $firstbootstate) 2>&1 | out-null 

If ($vmenvstate.Exists) {
    # If state file exists, nothing will happen.
    Write-Output "State file exists. Nothing to do....."
}
Else {
    #Here I am executing the whole script
    <Script Contents>
    #and in the end writing to state file
    Write-Output $vmdate":FirstBoot Complete" >> $firstbootstate
}

8. Loging

Of course it makes sense to do some loging in the script.  Nothing fancy here, I just capture current date to the variable and then writing some output to log file.

$vmlog = 'C:\Program Files\vmenv\vmenv.log'
$vmdate = Get-Date -Format "MMddyyyy-hh:mm"
Write-Output $vmdate": SOME INFO" >> $vmlog

9. Scheduling

I am using standard Windows Task scheduler to schedule this script to run on boot. Here is the command I used.

schtasks /create /tn "VappSet" /sc onstart /delay 0000:30 /rl highest /ru system /tr "powershell.exe -file 'C:\Program Files\vmenv\vappSet.ps1'"

10. Cleanup and export

Once all scripts are placed and task is scheduled its time to clean up and export.

  • Make sure Network settings are not configured on VM, set them to use DHCP.
  • Configure some standard password for the Administrator user.
  • Remove files which are not needed.
  • Shutdown the VM and export it as OVF.

Outcome

As an outcome you will have a template which on import will present you with a nice wizard like this one

Scripts

If you want to use the scripts you can take latest version from my github page, otherwise just copy from here.

"C:\Program Files\VMware\VMware Tools\vmtoolsd.exe" --cmd="info-get guestinfo.ovfEnv" > "C:\Program Files\vmenv\vmenv.xml"
#############################################################################
# Script Purpose: vApp settings	
# Author: Aram Avetisyan (@how2vm)
# Version: 2							          
# Release Date: 06.03.2018						
# Dependency: getvmenv.bat and unattend_no.xml				  
#############################################################################

#File Paths
$firstbootstate = 'C:\Program Files\vmenv\firstboot.state'
$vmlog = 'C:\Program Files\vmenv\vmenv.log'
$getenvbat = 'C:\Program` Files\vmenv\getvmenv.bat'
$vmenvxml = 'C:\Program Files\vmenv\vmenv.xml'
$sysprepnofile = 'C:\Program Files\vmenv\unattend_no.xml'
$sysprepokfile = 'C:\Program Files\vmenv\unattend_ok.xml'
$sysprepexe = "C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /reboot /unattend:'$sysprepokfile'"

# Check Execution state
($vmenvstate = Get-ChildItem $firstbootstate) 2>&1 | out-null 

If ($vmenvstate.Exists) {
    # If state file exists, nothing will happen.
    Write-Output "State file exists. Nothing to do....."
}
Else {
    # Generate Timestamp, Write to Log file and export XML config file
    $vmdate = Get-Date -Format "MMddyyyy-hh:mm"
    Write-Output $vmdate": Fetching config XMLs" >> $vmlog
    Invoke-Expression -Command $getenvbat

    # Import XMLs save those as variables.
    [xml]$vmenv = Get-Content $vmenvxml
    [xml]$vmsysprepenv = Get-Content $sysprepnofile
    # Collect Variables from vmenv XML
    $vmIP = $vmenv.Environment.PropertySection.Property | ?{ $_.key -like '*_IP*' } | select -expand value
    $vmNetmask = $vmenv.Environment.PropertySection.Property | ?{ $_.key -like '*Netmask*' } | select -expand value
    $vmGW = $vmenv.Environment.PropertySection.Property | ?{ $_.key -like '*Gateway*' } | select -expand value
    $vmHostname = $vmenv.Environment.PropertySection.Property | ?{ $_.key -like '*Hostname*' } | select -expand value
    $vmDNS = $vmenv.Environment.PropertySection.Property | ?{ $_.key -like '*DNS*' } | select -expand value

    # Replace Hostname in Sysprep file and write it to disk
    # If you ahve added nodes to sysprep, make sure array element number is fixed.
    $vmsysprepenv.unattend.settings.Component[2].Computername = "$vmHostname"
    $vmsysprepenv.Save($sysprepokfile)


    # Fetch Network interface name
    $ifname = Get-NetAdapter | Select -expand Name
    #Configure network
    $vmdate = Get-Date -Format "MMddyyyy-hh:mm"
    Write-Output $vmdate": Configuring Network settings" >> $vmlog
    New-NetIPAddress –InterfaceAlias $ifname –IPAddress $vmIP –PrefixLength $vmNetmask -DefaultGateway $vmGW
    Set-DnsClientServerAddress -InterfaceAlias $ifname -ServerAddresses $vmDNS

    # Execute sysprep to change SID and set hostname
    $vmdate = Get-Date -Format "MMddyyyy-hh:mm"
    Write-Output $vmdate": Setting a 10 Seconds timer before rebooting" >> $vmlog
    Write-Output "Sysprep will be executed in 20 seconds. A reboot will follow."
    Start-Sleep -s 20
    $vmdate = Get-Date -Format "MMddyyyy-hh:mm"
    Write-Output $vmdate":FirstBoot Complete" >> $firstbootstate
    Invoke-Expression -Command $sysprepexe
}
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <settings pass="generalize">
        <component name="Microsoft-Windows-PnpSysprep" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
            <PersistAllDeviceInstalls>true</PersistAllDeviceInstalls>
        </component>
    </settings>
    <settings pass="specialize">
        <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
            <RunSynchronous>
                <RunSynchronousCommand wcm:action="add">
                    <Description>Disable create user account</Description>
                    <Path>reg add HKLM\Software\Microsoft\Windows\CurrentVersion\Setup\OOBE /v UnattendCreatedUser /t REG_DWORD /d 1 /f</Path>
                    <Order>1</Order>
                </RunSynchronousCommand>
            </RunSynchronous>
        </component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
            <RegisteredOwner></RegisteredOwner>
            <RegisteredOrganization>HOW2VM</RegisteredOrganization>
            <TimeZone>Central Standard Time</TimeZone>
            <Computername>*</Computername>
            <OOBE>			
                <HideEULAPage>true</HideEULAPage>
                <NetworkLocation>Work</NetworkLocation>
                <ProtectYourPC>3</ProtectYourPC>
                <SkipUserOOBE>true</SkipUserOOBE>				
            </OOBE>
        </component>
    </settings>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
            <InputLocale>en-US</InputLocale>
            <SystemLocale>en-US</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UserLocale>en-US</UserLocale>
        </component>
    </settings>
    <cpi:offlineImage cpi:source="wim:c:/users/administrator/documents/install.wim#Windows Server 2016 SERVERDATACENTER" xmlns:cpi="urn:schemas-microsoft-com:cpi" />
</unattend>

Closing word

This was a nice learning experience for me, and I actually liked Powershell a lot. A usual, I don’t claim that this is the best way to do things, but it works. If you have any comments on how I can improve this, let me know down in the comments.

The following two tabs change content below.
Aram Avetisyan is an IT specialist with more than 18 years experience. He has rich background in various IT related fields like Cloud, Virtualization and SDN. He holds several industry level certifications including but not limited to VCIX-DCV, VCIX-NV. He is also a vEXPERT in years 2014-2021.

About Aram Avetisyan

Aram Avetisyan is an IT specialist with more than 18 years experience. He has rich background in various IT related fields like Cloud, Virtualization and SDN. He holds several industry level certifications including but not limited to VCIX-DCV, VCIX-NV. He is also a vEXPERT in years 2014-2021.
Bookmark the permalink.

14 Comments

  1. This entire process doesn’t seem to be working for me. I’ve followed all the steps. When I deploy my .OVF I get the customization UI and I enter the network & hostname info. But, sysprep never runs on the newly provisioned VM. Not sure what I’m missing

    • Is it only sysprep which does not run? What about every other setting? Assuming you are using the script provided above, sysprep is only executed, to change SID and hostname. Check C:\Program Files\vmenv\ to see if all template and generated files exist. Also check the log file to see what exactly is not being executed.

  2. Whats seems to be missing is the firstboot.state file and the .log file never seems to get written.

  3. I forgot to mention that the $sysprepokfile “unattend_ok.xml” is not present either

    • Missing firstboot.state, .log and unattend_ok.xml files just mean the script was never executed. All those files are being created as part of the execution. Points 6,7 and 8 described above. Initially you should only have 3 files in your folder: getvmenv.bat, unattend_no.xml and vappSet.ps1. Then you need to schedule the script to run on next boot. (point 9 in the article) . Are you sure the scheduling was done ? you can check if the task is there in Windows Task Scheduler.

  4. I’m 100% certain that the task is scheduled to run at startup. I taking another look at everything this morning. Actually as I write this I’m again exporting my master image to an .OVF

  5. Question, this will not work for an OVA correct? it has to be with an OVF?

  6. Where is the unattend_ok.xml that you used or how does it come into play here?

    • unattend_ok.xml is being generated by vappSet.ps1 script on lines 31-44 and then being consumed by sysprep on line 62, of the same script.

      • Got it! Also one more quick question. I’m no really an answer file expert or anything, any thoughts on the way to skip the license prompt and go ahead and set the Administrator password? TIA!

        • I am not sure i understand which part of the process are you referring to.
          In none of the steps during the process described, you are required to provide License key. Even if you will go run sysprep by hand it should not ask for license key, it should just boot, and then ask you to activate windows.

          • Interesting, it must be by template but it asks for a password as part of OOBE and a license key as well. I’ll keep looking. Thanks.

Leave a Reply

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