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.
Latest posts by Aram Avetisyan (see all)
- Make Youtube Videos About Technology? Why not… The Cross-Cloud Guy - October 7, 2021
- Automating (NSX-T) REST API using Ansible URI module - December 29, 2020
- Quick Reference: Create Security Policy with Firewall Rules using NSX-T Policy API - May 4, 2020
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.
Whats seems to be missing is the firstboot.state file and the .log file never seems to get written.
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.
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
Let us know if it eventually worked 🙂
Question, this will not work for an OVA correct? it has to be with an OVF?
there is no reason this should not work with OVA, but I never tested it myself.
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.