| | |

‘Mass’ sysprep your Windows VMs for Nutanix AHV

9 min read

WindowsAfter reading this excellent blogpost from Andrew Nelson (Distributed Systems Specialist at Nutanix) on Acropolis Image and Cloning Primer for Automation and combining that with this blogpost from Raghu Nandan (Director of Product Management) on Setting up Citrix XA/XD with an Acropolis Hypervisor cluster I figured we were missing one vendor on this list.. Microsoft Windows Server/Client OSs.

Although the blogpost from Raghu offers a solid way to deploy your Windows based VM(s) on Nutanix AHV I’ve been working on a way to do so without the need of client components on the VM side of things.

One way to do so is utilising Windows native PowerShell which is pretty straightforward except for one little caveat: Remote PowerShell does not play nicely with non-domain joined machines as there’s no easy way to authenticate. It could work with SSL certificates etc but that’s adding more complexity as it is so I went for a solution that could be executed with PsExec which is a light-weight telnet-replacement that lets you execute processes on other systems, complete with full interactivity for console applications, without having to manually install client software. PsExec’s most powerful uses include launching interactive command-prompts on remote systems and remote-enabling tools like IpConfig that otherwise do not have the ability to show information about remote systems.

The good thing about working with sysprep is that it will offer a way to execute it unattended wit an unattended.xml. You can describe your parameters in this XML file and sysprep.exe will parse them into the right commands. A detailed explanation of how this would work can be found on the TechNet forums, there’s an easy way to validate your unattended.xml file to using the Windows System Image Manager (Windows SIM).

With that being said, I’ve came up with the following PowerShell script to do a remote execution of a sysprep for your Windows VM:

 

#Requires -Version 2.0
#This File is in Unicode format. Do not edit in an ASCII editor.

<#
.SYNOPSIS
 Runs an unattended version of Sysprep.
.DESCRIPTION
 This script can be used to run an unattended version of Sysprep, focussed on Acropolis Hypervisor.
 Prerequisites: 
 Windows 7/Windows 2008R2 and up
 Set your execution policy to unrestricted
.PARAMETER ComputerName
 The hostname for this VM
.PARAMETER DomainName
 The Domain name for the domain join
.PARAMETER DomainUser
 Username for the domain join, need to be in domain\username format
.PARAMETER DomainPwd
 Password for domain join
.PARAMETER RegisteredName
 Setting the registered name for this VM
.PARAMETER Organisation
 Setting the registered organisation name for this VM
 .EXAMPLE
 PS C:\PSScript > PsExec.exe RunSysPrep_vx.x.ps1 -ComputerName "VDI001" -DomainName "contoso.local" -DomainUser "Contoso\administrator" -DomainPwd "*******" -RegisterNamed "Nutanix" -Organization "Nutanix Inc"
.INPUTS
 None. You cannot pipe objects to this script.
.OUTPUTS
 No objects are output from this script. 
.NOTES
 NAME: RunSysPrep_vx.x.ps1
 VERSION: 1.0
 AUTHOR: Kees Baggerman with help from Iain Brighton
 LASTEDIT: July, 2015
#>



[CmdletBinding()]
param(
 [Parameter(Mandatory = $True)][String]$ComputerName,
 [Parameter(Mandatory = $True)][String]$DomainName,
 [Parameter(Mandatory = $True)][String]$DomainUser,
 [Parameter(Mandatory = $True)][String]$RegisteredName,
 [Parameter(Mandatory = $True)][String]$Organization,
 [Parameter(Mandatory = $True)][Security.SecureString]$DomainPwd
 )
 

# Taking a secure password and converting to plain text
Function ConvertTo-PlainText( [security.securestring]$secure ) {
 $marshal = [Runtime.InteropServices.Marshal]
 $marshal::PtrToStringAuto( $marshal::SecureStringToBSTR($secure) )
}
 
# Set the path variable to the XML file needed for sys prep 
 $path = 'C:\Windows\Temp\unattend.xml'

# Check if the script has already been executed and abort if that's the case


If (Test-Path $path){
 # // File exists
 Exit
}Else{
 # // File does not exist

 
# create a template XML to hold data
$template = @'
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
 <settings pass="windowsPE">
 <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <SetupUILanguage>
 <UILanguage>en-US</UILanguage>
 </SetupUILanguage>
 <InputLocale>1033:00000409</InputLocale>
 <SystemLocale>en-US</SystemLocale>
 <UILanguage>en-US</UILanguage>
 <UserLocale>en-US</UserLocale>
 </component>
 <component name="Microsoft-Windows-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <Diagnostics>
 <OptIn>false</OptIn>
 </Diagnostics>
 <UserData>
 <AcceptEula>true</AcceptEula>
 <FullName>administrator</FullName>
 <Organization>$organizationname</Organization>
 </UserData>
 <EnableFirewall>true</EnableFirewall>
 </component>
 </settings>
 <settings pass="generalize">
 <component name="Microsoft-Windows-Security-SPP" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <SkipRearm>1</SkipRearm>
 </component>
 </settings>
 <settings pass="specialize">
 <component name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <SkipAutoActivation>true</SkipAutoActivation>
 </component>
 <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" 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>test</ComputerName>
 <ProductKey>HYF8J-CVRMY-CM74G-RPHKF-PW487</ProductKey>
 <TimeZone>Pacific Standard Time</TimeZone>
 </component>
 <component name="Microsoft-Windows-UnattendedJoin" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <Identification>
 <Credentials>
 <Domain>contoso.local</Domain>
 <Password>password</Password>
 <Username>administrator</Username>
 </Credentials>
 <JoinDomain>contoso.local</JoinDomain>
 <UnsecureJoin>False</UnsecureJoin>
 </Identification>
 </component>
 </settings>
 <settings pass="oobeSystem">
 <component name="Microsoft-Windows-International-Core" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <InputLocale>1033:00000409</InputLocale>
 <UILanguage>en-US</UILanguage>
 <UserLocale>en-US</UserLocale>
 </component>
 <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <RegisteredOwner>administrator</RegisteredOwner>
 <OOBE>
 <HideEULAPage>true</HideEULAPage>
 <NetworkLocation>Work</NetworkLocation>
 <ProtectYourPC>1</ProtectYourPC>
 <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
 <SkipMachineOOBE>true</SkipMachineOOBE>
 <SkipUserOOBE>true</SkipUserOOBE>
 </OOBE>
 <DisableAutoDaylightTimeSet>false</DisableAutoDaylightTimeSet>
 <FirstLogonCommands>
 <SynchronousCommand wcm:action="add">
 <RequiresUserInput>false</RequiresUserInput>
 <Order>1</Order>
 <Description>Disable Auto Updates</Description>
 <CommandLine>reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update" /v AUOptions /t REG_DWORD /d 1 /f</CommandLine>
 </SynchronousCommand>
 <SynchronousCommand wcm:action="add">
 <Description>Control Panel View</Description>
 <Order>2</Order>
 <CommandLine>reg add "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\ControlPanel" /v StartupPage /t REG_DWORD /d 1 /f</CommandLine>
 <RequiresUserInput>true</RequiresUserInput>
 </SynchronousCommand>
 <SynchronousCommand wcm:action="add">
 <Order>3</Order>
 <Description>Control Panel Icon Size</Description>
 <RequiresUserInput>false</RequiresUserInput>
 <CommandLine>reg add "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\ControlPanel" /v AllItemsIconView /t REG_DWORD /d 1 /f</CommandLine>
 </SynchronousCommand>
 </FirstLogonCommands>
 <AutoLogon>
 <Password>
 <Value>password</Value>
 <PlainText>true</PlainText>
 </Password>
 <Enabled>true</Enabled>
 <Username>administrator</Username>
 </AutoLogon>
 <UserAccounts>
 <LocalAccounts>
 <LocalAccount wcm:action="add">
 <Password>
 <Value>password</Value>
 <PlainText>true</PlainText>
 </Password>
 <Description></Description>
 <DisplayName>administrator</DisplayName>
 <Group>Administrators</Group>
 <Name>administrator</Name>
 </LocalAccount>
 </LocalAccounts>
 </UserAccounts>
 </component>
 </settings>
 <settings pass="offlineServicing">
 <component name="Microsoft-Windows-LUA-Settings" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <EnableLUA>false</EnableLUA>
 </component>
 </settings>
</unattend>

'@

# Writing the actual XML file with UTF8 encoding
$template | Out-File $Path -encoding UTF8

# Loading the XML file to modify values
$xml = New-Object XML
$xml.Load($path)

$xml.unattend.settings[0].component[1].UserData.FullName = $RegisteredName
$xml.unattend.settings[0].component[1].UserData.Organization = $Organization
$xml.unattend.settings[2].component[1].ComputerName = $ComputerName
$xml.unattend.settings[2].component[2].identification.JoinDomain = $DomainName
$xml.unattend.settings[2].component[2].identification.credentials.UserName = $DomainUser
$xml.unattend.settings[2].component[2].identification.credentials.Password = ConvertTo-PlainText $DomainPwd
$xml.unattend.settings[2].component[2].identification.credentials.Domain = $DomainName

# Saving the XML
$xml.Save($path)

# Enabling the default administrator account
net user administrator /active:yes

# Running SysPrep adding /mode:vm when running Windows 8 or above to speed up the sysprep process

$Version=(Get-WmiObject win32_operatingsystem).version 
 
if(!($version -eq "6.1")) 
{$argList = "/generalize /oobe /reboot /unattend:$path"} 
else 
{$argList = "/generalize /oobe /reboot /mode:vm /unattend:$path"}

Start-Process -FilePath C:\Windows\System32\Sysprep\Sysprep.exe -ArgumentList $argList
}

 

As you can see it generates the unattended.xml on the local system so the remote execution of sysprep can find the XML file. One thing that I like about this approach is that it’s more flexible in terms of your VM naming scheme, OU placement and settings as you’re able to dynamically change/adjust the settings per VM. This script was tested on Windows 2008R2 and Windows 7 and  comes ‘as-is’.

Pro-tip: If you think it’s too much work to launch SIM you could always try to use of the online generators for your unattended.xml which can be found here: http://windowsafg.no-ip.org/, please note that there are multiple options for your XML file depending on your Windows version so please choose the right OS before creating your xml file.

The following two tabs change content below.

Kees Baggerman

Kees Baggerman is Senior Technical Director — Performance & Solutions Engineering R&D at Nutanix, where he leads a global team responsible for defining how enterprise applications are delivered on the Nutanix platform. A former Citrix Technology Professional and NVIDIA Enterprise Platform Advisor, he has spent 15+ years driving EUC strategy and technical direction across architecture, product, and customer success. He has been writing here since 2011 — sharing what he learns at the intersection of platform engineering and enterprise IT.
Kees Baggerman

Kees Baggerman

Senior Technical Director at Nutanix - Former Citrix CTP - NVIDIA Enterprise Platform Advisor - 15+ years in EUC

Kees Baggerman is Senior Technical Director — Performance & Solutions Engineering R&D at Nutanix, where he leads a global team responsible for defining how enterprise applications are delivered on the Nutanix platform. A former Citrix Technology Professional and NVIDIA Enterprise Platform Advisor, he has spent 15+ years driving EUC strategy and technical direction across architecture, product, and customer success. He has been writing here since 2011 — sharing what he learns at the intersection of platform engineering and enterprise IT.

Similar Posts

2 Comments

  1. Hi Kees,

    You already have the XML template in a string so there’s no need to save it to disk and then load it as xml. It’s much easier to simply cast it as xml:

    [codesyntax lang="powershell" lines="normal"]
    $xml = [xml]$template
    [/codesyntax]

    As for setting the properties in XML, I don’t like hardcoded arrays since it makes the code more difficult to read (and I do read a script before executing it). It’s also more error prone as the order might change in the future.

    So instead of this:
    $xml.unattend.settings[0].component[1].UserData.FullName = $RegisteredName

    I recommend this:

    [codesyntax lang="powershell" lines="normal"]
    $ns = New-Object Xml.XmlNamespaceManager($xml.NameTable)
    $ns.AddNamespace(“unattend”, $xml.DocumentElement.NamespaceURI)

    $msWinSetup = $xml.SelectSingleNode(“//unattend:settings[@pass=’windowsPE’]/unattend:component[@name=’Microsoft-Windows-Setup’]”, $ns)
    $msWinSetup.UserData.FullName = $RegisteredName
    $msWinSetup.UserData.Organization = $Organization

    $msWinShellSetup = $xml.SelectSingleNode(“//unattend:settings[@pass=’specialize’]/unattend:component[@name=’Microsoft-Windows-Shell-Setup’]”, $ns)
    $msWinShellSetup.ComputerName = $ComputerName

    $msWinUnattendJoin = $xml.SelectSingleNode(“//unattend:settings[@pass=’specialize’]/unattend:component[@name=’Microsoft-Windows-UnattendedJoin’]”, $ns)
    $msWinUnattendJoin.identification.JoinDomain = $DomainName
    $msWinUnattendJoin.identification.credentials.UserName = $DomainUser
    $msWinUnattendJoin.identification.credentials.Password = ConvertTo-PlainText $DomainPwd
    $msWinUnattendJoin.identification.credentials.Domain = $DomainName
    [/codesyntax]

Leave a Reply

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

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