[Nutanix] Mass Deployment of Windows 2016 on AHV using Sysprep, AnswerFile and Powershell

In the past, I have created detailed posts on Sysprep and answer files to deploy customized Image of Windows 2012 R2 which also can be used to Mass Deploy Windows OS. Please have a look here and here. I’m quite grateful to achieve automation of that level. It is nearly six month back post. In the recent project, I was asked to build a template (The way we do in VMware) and use user inputs to deploy Windows Server 2016 to mass deploy it. Not a difficult task to achieve if you are willing to focus. In olden days there was saying “Where there is a will, there is way” Now it is

Where there is a will, there is a skill “

Well, this post is going to be bit detailed. I have created two parts to this post. The first part, I will be focusing on creating the answer file. But didn’t I taught you how to build one here? Yes, it is but when you Sysprep image you need different (little different approach). If you are involved in customizing Windows build, you reasonably would prefer to skip this section. In the Second portion, I’ll be discussing the Nutanix cmdlets and how to use it to pass answer file

1) Answer file for Sysprep image

This answer file differs in several ways, particularly around configuration passes.

To emphasize, Windows PE is not required at all since Windows Edition, Disk size, Partition is already there in Sysprep image

Windows PE Pass is blank
Windows PE Pass is blank

So in my answer file (above), you will see I have not configured anything. So allow me to move to the new section.

Pass 5 Specialize

Usually, in vCenter, while cloning VM, you specify VM Name and IP Address. While there are many other options available to customize, we don’t use them all in most of the cases. To give a unique name to the VM,  you need to use  Microsoft-Windows-Shell-Setup. As my screen capture below it is amd64_Microsoft-Windows-Shell-Setup_neutral

In Specialize Configuration Pass Microsoft-Windows-Shell-Setup-Netural is being configured
In Specialize Configuration Pass Microsoft-Windows-Shell-Setup-Neutral is being configured

Others settings in the specialize sections I have explained in the preceding posts. Therefore, I won’t repeat. Before I move to next pass, I would like to bring to your attention on configuring the default gateway. The default gateway configuration as per the Microsoft document and Help files doesn’t match. The default gateway is referred as Route in Microsoft reference material. In the previous post, I learned this, but I have not discussed it there. Please make a note of it. I have not come across a post/link which explains if this is accepted. I’m happy to be corrected.

Here is the post which gives me the hint how to use default gateway format. But I found that by accident while searching for another error. Reference Post -> http://www.korock.net/?p=62

Route Format/Default Gateway Format in Answer file
Route Format/Default Gateway Format in Answer file

Pass 7 oobe System

This pass is executed after the second and final reboot of Sysprep image. The first thing you must do is language selection as shown below.

"<yoastmark

Next important thing and it is where I struggled a bit using EULA selection. Unless you select true in HideEULAPage as shown below, it will keep popping up

EULA selection is required in OOBE section
EULA selection is required in OOBE section

I have not explained Other sections in this pass to avoid repetition. So this is all you have to do to create an answer file.

Here is the detailed answer file

&amp;amp;lt;?xml version="1.0" encoding="utf-8"?&amp;amp;gt;
&amp;amp;lt;unattend xmlns="urn:schemas-microsoft-com:unattend"&amp;amp;gt;
 &amp;amp;lt;servicing&amp;amp;gt;&amp;amp;lt;/servicing&amp;amp;gt;
 &amp;amp;lt;settings pass="specialize"&amp;amp;gt;
 &amp;amp;lt;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"&amp;amp;gt;
 &amp;amp;lt;ComputerName&amp;amp;gt;sss2017&amp;amp;lt;/ComputerName&amp;amp;gt;
 &amp;amp;lt;AutoLogon&amp;amp;gt;
 &amp;amp;lt;Password&amp;amp;gt;
 &amp;amp;lt;Value&amp;amp;gt;VgBNAHcAYQByAGUAMQAhACoAKAAqAEAAUABhAHMAcwB3AG8AcgBkAA==&amp;amp;lt;/Value&amp;amp;gt;
 &amp;amp;lt;PlainText&amp;amp;gt;false&amp;amp;lt;/PlainText&amp;amp;gt;
 &amp;amp;lt;/Password&amp;amp;gt;
 &amp;amp;lt;LogonCount&amp;amp;gt;1&amp;amp;lt;/LogonCount&amp;amp;gt;
 &amp;amp;lt;Username&amp;amp;gt;preetam&amp;amp;lt;/Username&amp;amp;gt;
 &amp;amp;lt;Enabled&amp;amp;gt;true&amp;amp;lt;/Enabled&amp;amp;gt;
 &amp;amp;lt;Domain&amp;amp;gt;shsee.org&amp;amp;lt;/Domain&amp;amp;gt;
 &amp;amp;lt;/AutoLogon&amp;amp;gt;
 &amp;amp;lt;/component&amp;amp;gt;
 &amp;amp;lt;component name="Microsoft-Windows-ServerManager-SvrMgrNc" 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"&amp;amp;gt;
 &amp;amp;lt;DoNotOpenServerManagerAtLogon&amp;amp;gt;true&amp;amp;lt;/DoNotOpenServerManagerAtLogon&amp;amp;gt;
 &amp;amp;lt;/component&amp;amp;gt;
 &amp;amp;lt;component name="Microsoft-Windows-TerminalServices-LocalSessionManager" 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"&amp;amp;gt;
 &amp;amp;lt;fDenyTSConnections&amp;amp;gt;false&amp;amp;lt;/fDenyTSConnections&amp;amp;gt;
 &amp;amp;lt;/component&amp;amp;gt;
 &amp;amp;lt;component name="Networking-MPSSVC-Svc" 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"&amp;amp;gt;
 &amp;amp;lt;DomainProfile_EnableFirewall&amp;amp;gt;false&amp;amp;lt;/DomainProfile_EnableFirewall&amp;amp;gt;
 &amp;amp;lt;PrivateProfile_EnableFirewall&amp;amp;gt;false&amp;amp;lt;/PrivateProfile_EnableFirewall&amp;amp;gt;
 &amp;amp;lt;PublicProfile_EnableFirewall&amp;amp;gt;false&amp;amp;lt;/PublicProfile_EnableFirewall&amp;amp;gt;
 &amp;amp;lt;/component&amp;amp;gt;
 &amp;amp;lt;component name="Microsoft-Windows-TCPIP" 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"&amp;amp;gt;
 &amp;amp;lt;Interfaces&amp;amp;gt;
 &amp;amp;lt;Interface wcm:action="add"&amp;amp;gt;
 &amp;amp;lt;Identifier&amp;amp;gt;Ethernet&amp;amp;lt;/Identifier&amp;amp;gt;
 &amp;amp;lt;Ipv4Settings&amp;amp;gt;
 &amp;amp;lt;DhcpEnabled&amp;amp;gt;false&amp;amp;lt;/DhcpEnabled&amp;amp;gt;
 &amp;amp;lt;RouterDiscoveryEnabled&amp;amp;gt;true&amp;amp;lt;/RouterDiscoveryEnabled&amp;amp;gt;
 &amp;amp;lt;Metric&amp;amp;gt;30&amp;amp;lt;/Metric&amp;amp;gt;
 &amp;amp;lt;/Ipv4Settings&amp;amp;gt;
 &amp;amp;lt;UnicastIpAddresses&amp;amp;gt;
 &amp;amp;lt;IpAddress wcm:action="add" wcm:keyValue="1"&amp;amp;gt;192.168.1.235/24&amp;amp;lt;/IpAddress&amp;amp;gt;
 &amp;amp;lt;/UnicastIpAddresses&amp;amp;gt;
 &amp;amp;lt;Routes&amp;amp;gt;
 &amp;amp;lt;Route wcm:action="add"&amp;amp;gt;
 &amp;amp;lt;Identifier&amp;amp;gt;10&amp;amp;lt;/Identifier&amp;amp;gt;
 &amp;amp;lt;Metric&amp;amp;gt;20&amp;amp;lt;/Metric&amp;amp;gt;
 &amp;amp;lt;NextHopAddress&amp;amp;gt;192.168.1.1&amp;amp;lt;/NextHopAddress&amp;amp;gt;
 &amp;amp;lt;Prefix&amp;amp;gt;0.0.0.0/0&amp;amp;lt;/Prefix&amp;amp;gt;
 &amp;amp;lt;/Route&amp;amp;gt;
 &amp;amp;lt;/Routes&amp;amp;gt;
 &amp;amp;lt;/Interface&amp;amp;gt;
 &amp;amp;lt;/Interfaces&amp;amp;gt;
 &amp;amp;lt;/component&amp;amp;gt;
 &amp;amp;lt;component name="Microsoft-Windows-DNS-Client" 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"&amp;amp;gt;
 &amp;amp;lt;DNSSuffixSearchOrder&amp;amp;gt;
 &amp;amp;lt;DomainName wcm:action="add" wcm:keyValue="1"&amp;amp;gt;vzare.com&amp;amp;lt;/DomainName&amp;amp;gt;
 &amp;amp;lt;/DNSSuffixSearchOrder&amp;amp;gt;
 &amp;amp;lt;Interfaces&amp;amp;gt;
 &amp;amp;lt;Interface wcm:action="add"&amp;amp;gt;
 &amp;amp;lt;DNSServerSearchOrder&amp;amp;gt;
 &amp;amp;lt;IpAddress wcm:action="add" wcm:keyValue="1"&amp;amp;gt;192.168.1.110&amp;amp;lt;/IpAddress&amp;amp;gt;
 &amp;amp;lt;/DNSServerSearchOrder&amp;amp;gt;
 &amp;amp;lt;Identifier&amp;amp;gt;Ethernet&amp;amp;lt;/Identifier&amp;amp;gt;
 &amp;amp;lt;DisableDynamicUpdate&amp;amp;gt;false&amp;amp;lt;/DisableDynamicUpdate&amp;amp;gt;
 &amp;amp;lt;DNSDomain&amp;amp;gt;shsee.org&amp;amp;lt;/DNSDomain&amp;amp;gt;
 &amp;amp;lt;EnableAdapterDomainNameRegistration&amp;amp;gt;true&amp;amp;lt;/EnableAdapterDomainNameRegistration&amp;amp;gt;
 &amp;amp;lt;/Interface&amp;amp;gt;
 &amp;amp;lt;/Interfaces&amp;amp;gt;
 &amp;amp;lt;DNSDomain&amp;amp;gt;shsee.org&amp;amp;lt;/DNSDomain&amp;amp;gt;
 &amp;amp;lt;/component&amp;amp;gt;
 &amp;amp;lt;component name="Microsoft-Windows-UnattendedJoin" 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"&amp;amp;gt;
 &amp;amp;lt;Identification&amp;amp;gt;
 &amp;amp;lt;Credentials&amp;amp;gt;
 &amp;amp;lt;Domain&amp;amp;gt;shsee.org&amp;amp;lt;/Domain&amp;amp;gt;
 &amp;amp;lt;Password&amp;amp;gt;VMware1!&amp;amp;lt;/Password&amp;amp;gt;
 &amp;amp;lt;Username&amp;amp;gt;preetam&amp;amp;lt;/Username&amp;amp;gt;
 &amp;amp;lt;/Credentials&amp;amp;gt;
 &amp;amp;lt;JoinDomain&amp;amp;gt;shsee.org&amp;amp;lt;/JoinDomain&amp;amp;gt;
 &amp;amp;lt;/Identification&amp;amp;gt;
 &amp;amp;lt;/component&amp;amp;gt;
 &amp;amp;lt;/settings&amp;amp;gt;
 &amp;amp;lt;settings pass="oobeSystem"&amp;amp;gt;
 &amp;amp;lt;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"&amp;amp;gt;
 &amp;amp;lt;UserAccounts&amp;amp;gt;
 &amp;amp;lt;AdministratorPassword&amp;amp;gt;
 &amp;amp;lt;Value&amp;amp;gt;VgBNAHcAYQByAGUAMQAhACoAKAAqAEAAQQBkAG0AaQBuAGkAcwB0AHIAYQB0AG8AcgBQAGEAcwBzAHcAbwByAGQA&amp;amp;lt;/Value&amp;amp;gt;
 &amp;amp;lt;PlainText&amp;amp;gt;false&amp;amp;lt;/PlainText&amp;amp;gt;
 &amp;amp;lt;/AdministratorPassword&amp;amp;gt;
 &amp;amp;lt;LocalAccounts&amp;amp;gt;
 &amp;amp;lt;LocalAccount wcm:action="add"&amp;amp;gt;
 &amp;amp;lt;Password&amp;amp;gt;
 &amp;amp;lt;Value&amp;amp;gt;VgBNAHcAYQByAGUAMQAhACoAKAAqAEAAUABhAHMAcwB3AG8AcgBkAA==&amp;amp;lt;/Value&amp;amp;gt;
 &amp;amp;lt;PlainText&amp;amp;gt;false&amp;amp;lt;/PlainText&amp;amp;gt;
 &amp;amp;lt;/Password&amp;amp;gt;
 &amp;amp;lt;Description&amp;amp;gt;Local Admin Account&amp;amp;lt;/Description&amp;amp;gt;
 &amp;amp;lt;DisplayName&amp;amp;gt;Super Admin Account&amp;amp;lt;/DisplayName&amp;amp;gt;
 &amp;amp;lt;Group&amp;amp;gt;administrators&amp;amp;lt;/Group&amp;amp;gt;
 &amp;amp;lt;Name&amp;amp;gt;super_07&amp;amp;lt;/Name&amp;amp;gt;
 &amp;amp;lt;/LocalAccount&amp;amp;gt;
 &amp;amp;lt;/LocalAccounts&amp;amp;gt;
 &amp;amp;lt;/UserAccounts&amp;amp;gt;
 &amp;amp;lt;OOBE&amp;amp;gt;
 &amp;amp;lt;HideEULAPage&amp;amp;gt;true&amp;amp;lt;/HideEULAPage&amp;amp;gt;
 &amp;amp;lt;/OOBE&amp;amp;gt;
 &amp;amp;lt;FirstLogonCommands&amp;amp;gt;
 &amp;amp;lt;SynchronousCommand wcm:action="add"&amp;amp;gt;
 &amp;amp;lt;CommandLine&amp;amp;gt;%WINDIR%\System32\WindowsPowerShell\v1.0\PowerShell.exe -command Import-Module ServerManager; Add-WindowsFeature RSAT-Role-Tools; Add-WindowsFeature RSAT-DNS-Server; Add-WindowsFeature Telnet-Client&amp;amp;lt;/CommandLine&amp;amp;gt;
 &amp;amp;lt;Description&amp;amp;gt;RSAT Tools&amp;amp;lt;/Description&amp;amp;gt;
 &amp;amp;lt;Order&amp;amp;gt;1&amp;amp;lt;/Order&amp;amp;gt;
 &amp;amp;lt;RequiresUserInput&amp;amp;gt;false&amp;amp;lt;/RequiresUserInput&amp;amp;gt;
 &amp;amp;lt;/SynchronousCommand&amp;amp;gt;
 &amp;amp;lt;/FirstLogonCommands&amp;amp;gt;
 &amp;amp;lt;/component&amp;amp;gt;
 &amp;amp;lt;component name="Microsoft-Windows-International-Core" 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"&amp;amp;gt;
 &amp;amp;lt;InputLocale&amp;amp;gt;en-US&amp;amp;lt;/InputLocale&amp;amp;gt;
 &amp;amp;lt;SystemLocale&amp;amp;gt;en-US&amp;amp;lt;/SystemLocale&amp;amp;gt;
 &amp;amp;lt;UILanguage&amp;amp;gt;en-US&amp;amp;lt;/UILanguage&amp;amp;gt;
 &amp;amp;lt;UserLocale&amp;amp;gt;en-US&amp;amp;lt;/UserLocale&amp;amp;gt;
 &amp;amp;lt;/component&amp;amp;gt;
 &amp;amp;lt;/settings&amp;amp;gt;
 &amp;amp;lt;cpi:offlineImage cpi:source="wim:c:/users/preetam/downloads/install.wim#Windows Server 2016 SERVERSTANDARD" xmlns:cpi="urn:schemas-microsoft-com:cpi" /&amp;amp;gt;
&amp;amp;lt;/unattend&amp;amp;gt;

2) Nutanix PowerShell CMDlets

Before I discuss PowerShell provisioning method let me tell you there is another easy but slower approach to provision VM. Open prism portal, create VM and right at the bottom select checkbox and paste the answer file. It will work.

Create VM in AHV using Answer file and Sysprep Image
Create VM in AHV using Answer file and Sysprep Image

As I said, it is the traditional and less optimal method. Let me walk you through PowerShell script.

 
Add-PSSnapin -Name NutanixCmdletsPSSnapin
Connect-NTNXCluster -server 192.168.1.114 -UserName admin -AcceptInvalidSSLCerts -ForcedConnection
$VMNamestr=read-host "Enter the name of virtual machine"
$IP=read-host "Please enter IP for this Machine"
#casting into strings
$VMName=[string]$VMNamestr
$IPaddress=[string]$IP
$AnswerFile="C:\AHVAnswer\JustTry.xml"
$xml = New-Object XML
$xml.Load($AnswerFile)
$xml.unattend.settings[0].component[4].Interfaces.Interface.UnicastIpAddresses.IpAddress.'#text'=$IPaddress
$xml.unattend.settings[0].component[0].ComputerName=$VMName
$xml.Save($AnswerFile)
$sysprepdata=get-content $AnswerFile
$ws2016=Get-NTNXVM -VmId 0005470e-0be1-8f27-36dd-0050569dae35::9b277a60-b22b-4026-96a6-d617b7b9e01d
$custom = New-NTNXObject -Name VMCloneDTO
$VMCustomizationConfigDTO=New-NTNXObject -Name VMCustomizationConfigDTO
$VMCustomizationConfigDTO[1].freshInstall=$false
$VMCustomizationConfigDTO[1].userdata=$sysprepdata
$custom.vmCustomizationConfig=$VMCustomizationConfigDTO[1]
$spec = New-NTNXObject -Name VMCloneSpecDTO
$spec.name=$VMName
Clone-NTNXVirtualMachine -Vmid $ws2016.vmId -VmCustomizationConfig $custom.vmCustomizationConfig -SpecList $spec
Start-Sleep -Seconds 18
$VM2PowerOn=Get-NTNXVM | where{$_.vmname -eq $VMName}
Set-NTNXVMPowerOn -Vmid $VM2PowerOn.vmId
 
Unless I explain every line, I won’t be doing justice to this post.
To being with line 1, I’m loading Nutanix cmdlets.
In the second line, I’m connecting to Nutanix cluster
In the third and fourth line, I’m taking user inputs for username and IP Address for the VM
Line 6 and 7 I’m casting them into string
In the 8th line, I’m providing the answer file path
In 9th line, I have created XML object, following i.e. 10th line I have loaded the answer file
Line 11 and 12 are updating answer file with unique IP Address and VM name which was a user input in line 3 and 4
Finally, we save the changes made in XML in line 13
So far so good.
Now in line 14, we take the updated answer file in the variable.
Here we need to know how to take answer file into the variable and pass it to Clone-NTNXVirtualMachine command. Let’s work on it.
In line 15 is where the first NTNX cmdlet is used, I’m pulling vmId of the sysprep’ed image.
In line 16 we have to create VMCloneDTO object.
VMCloneDTO, VMCustomizationConfigDTO and VMCloneSpecDTO objects are necessary. Well, why?
VMCustomizationConfigDTO
VmCustomizationConfig parameter in Clone-NTNXVirtual machine (refer Line 23) needs VMCustomizationConfigDTO parameter. Given this fact you must create this object. When you create this object and explore the parameters of the object, you will see five attributes. I have illustrated below by mapping the parameters with GUI (Refer image above in case you are not able to co-relate the picture) parameters for easier understanding.
VMCustomizationConfigDTO object created to insert Answer file
VMCustomizationConfigDTO object created to insert Answer file

Now If you refer to line 18, where I’m declaring freshInstall as $false since this is not fresh but cloned image. Likewise, if you follow line 19, it is where I inserted the answer file into userdata. These two lines are all you need to update in VMCustomizationConfigDTO object.

VMCloneDTO

VMClone DTO is the parent object of VMCustomizationConfigDTO. So this object creation is next logical step. When you create this object and explore its parameter, you will discover there is a VMCustomizationConfig parameter. I have shown it below by using test object which you can match it with line 16.

image

Inputs which were pushed in line 18,19 needs to be updated in VMCustomizationConfigDTO. I have done it in line 20.

In line 21, I created VMCloneSpecDTO object. Primarily this object is used to name the VM which you can see in line 22; the input we received in line 3, which is later on passed into line 6.

Line 22 is the actual command we have to run to clone the VM. The great thing about the command is, it runs in seconds, unlike VMware template which is influenced by storage characteristics.

Line 23, 24 are the command used to power on the recently created Virtual machine.

Summary

All things considered now you can deploy, customize Windows 2016 server using Sysprep, Answer file and Powershell. We saw how Answer file gets pruned down when we use Sysprep image. Moreover, it also reduced deployment time. Ultimately Nutanix cmdlets come handy when cloning VM using the answer file.