Introduction

Unattended installation of Windows Server 2016 virtual machines is explained below

Windows Server 2016 installation can be fully automated by using an unattend.xml file and using PowerShell code. This article demonstrates a typical unattend.xml file that can be armed with user preferences through PowerShell and perform the zero-touch installation. The only prerequisite is a Gen 2 syspreped version of Windows Server 2016 in either Core or Desktop Experience installation. The unattend.xml file information and the PowerShell code will be displayed in this article and you can copy and use it in your own environment. This installation method truly allows for a light out installation even from a single Core installation of Windows Server and a simple PowerShell console.

The installation steps are pretty straightforward:

  • Create an installation folder in one of the host server drives and copy the syspreped image and the unattend.xml file in it
  •  Review the PowerShell code and arm the appropriate variables.
  •  Start the rest of the installation by using the PowerShell commands.
When the installation starts, the PowerShell code will retrieve the host default virtual machine location and create a folder structure with the virtual machine name. It will copy the syspreped file and rename it accordingly. It will then create a new virtual machine with the name provided in the PowerShell Code and add the amount of memory and CPU cores specified. In order to copy the unattend.xml file, the PowerShell code will actually create a copy of the unattend.xml template and mount the copied VHD file and copy the unattend.xml in the root folder. When the virtual machine starts it will complete the Windows Server installation using the specified name as a Computer Name and the specified IP address and local administrator password in the PowerShell Code.

The unattend.xml

The sample unattend.xml file is displayed below. For highlighting purposes, the actual data that will be replaced by the PowerShell code has the number 1 added in the text. You can however add more customization options and customize the PowerShell code to replace those values.

#Copy everything after this line and save it as unattend.xml#

<?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="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">
  <SetupUILanguage>
  <UILanguage>en-US</UILanguage>
  </SetupUILanguage>
  <InputLocale>0409:00000409</InputLocale>
  <SystemLocale>en-US</SystemLocale>
  <UILanguage>en-US</UILanguage>
  <UILanguageFallback>en-US</UILanguageFallback>
  <UserLocale>en-US</UserLocale>
  </component>
  <component name="Microsoft-Windows-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">
  <UserData>
  <AcceptEula>true</AcceptEula>
  <FullName>1AdminAccount</FullName>
  <Organization>1Organization</Organization>
  </UserData>
  <EnableFirewall>true</EnableFirewall>
  <EnableNetwork>true</EnableNetwork>
  </component>
  </settings>
  <settings pass="offlineServicing">
  <component name="Microsoft-Windows-LUA-Settings" 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">
  <EnableLUA>true</EnableLUA>
  </component>
  </settings>
  <settings pass="generalize">
  <component name="Microsoft-Windows-Security-SPP" 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">
  <SkipRearm>1</SkipRearm>
  </component>
  </settings>
  <settings pass="specialize">
  <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">
  <InputLocale>0409:00000409</InputLocale>
  <SystemLocale>en-US</SystemLocale>
  <UILanguage>en-US</UILanguage>
  <UILanguageFallback>en-US</UILanguageFallback>
  <UserLocale>en-US</UserLocale>
  </component>
  <component name="Microsoft-Windows-Security-SPP-UX" 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">
  <SkipAutoActivation>true</SkipAutoActivation>
  </component>
  <component name="Microsoft-Windows-SQMApi" 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">
  <CEIPEnabled>0</CEIPEnabled>
   </component>
  <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">
  <ComputerName>1Name</ComputerName>
  <ProductKey>1ProductID</ProductKey>
  </component>
  <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">
  <Interfaces>
  <Interface wcm:action="add">
   <Identifier>1MacAddressDomain</Identifier>
  <Ipv4Settings>
  <DhcpEnabled>false</DhcpEnabled>
  <Metric>10</Metric>
  <RouterDiscoveryEnabled>false</RouterDiscoveryEnabled>
  </Ipv4Settings>
  <UnicastIpAddresses>
  <IpAddress wcm:action="add" wcm:keyValue="1">1IPDomain/24</IpAddress>
  </UnicastIpAddresses>
  <Routes>
  <Route wcm:action="add">
  <Identifier>1</Identifier>
  <Metric>10</Metric>
  <NextHopAddress>1DefaultGW</NextHopAddress>
  <Prefix>0.0.0.0/0</Prefix>
  </Route>
  </Routes>
  </Interface>
  </Interfaces>
  </component>
  <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">
  <Interfaces>
  <Interface wcm:action="add">
   <DNSServerSearchOrder>
  <IpAddress wcm:action="add" wcm:keyValue="1">1DNSServer</IpAddress>
  </DNSServerSearchOrder>
  <Identifier>1MACAddressDomain</Identifier>
   <EnableAdapterDomainNameRegistration>true</EnableAdapterDomainNameRegistration>
  <DisableDynamicUpdate>true</DisableDynamicUpdate>
  <DNSDomain>1DNSDomain</DNSDomain>
  </Interface>
  </Interfaces>
  <DNSDomain>1DNSDomain</DNSDomain>
  <UseDomainNameDevolution>true</UseDomainNameDevolution>
  </component>
  </settings>
  <settings pass="oobeSystem">
  <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">
  <OOBE>
  <HideEULAPage>true</HideEULAPage>
  <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
  <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
   <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
  <NetworkLocation>Work</NetworkLocation>
  <ProtectYourPC>1</ProtectYourPC>
  <SkipUserOOBE>true</SkipUserOOBE>
  <SkipMachineOOBE>true</SkipMachineOOBE>
  </OOBE>
  <UserAccounts>
  <LocalAccounts>
  <LocalAccount wcm:action="add">
  <Password>
  <Value>1AdminPassword</Value>
   <PlainText>True</PlainText>
  </Password>
  <Description></Description>
  <DisplayName>1AdminAccount</DisplayName>
  <Group>Administrators</Group>
   <Name>1AdminAccount</Name>
  </LocalAccount>
  </LocalAccounts>
  </UserAccounts>
  <RegisteredOrganization>1Organization</RegisteredOrganization>
  <RegisteredOwner>1Organization</RegisteredOwner>
  <DisableAutoDaylightTimeSet>false</DisableAutoDaylightTimeSet>
  <TimeZone>GTB Standard Time</TimeZone>
  <VisualEffects>
  <SystemDefaultBackgroundColor>2</SystemDefaultBackgroundColor>
  </VisualEffects>
  </component>
  <component name="Microsoft-Windows-ehome-reg-inf" 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">
  <RestartEnabled>true</RestartEnabled>
  </component>
  <component name="Microsoft-Windows-ehome-reg-inf" 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">
  <RestartEnabled>true</RestartEnabled>
    </component>
  </settings>
  <cpi:offlineImage cpi:source="wim:c:/server2016/sources/install.wim#Windows Server 2016 SERVERDATACENTERCORE" xmlns:cpi="urn:schemas-microsoft-com:cpi" />
</unattend>

 

#Copy everything above this line and save it as unattend.xml#

 

PowerShell Code

The PowerShell code is pretty easy to understand and comments are added in every line for you to fully understand what is going on.

#Arm the Variables, a bunch of them
 
#Cpu Cores in the VM
$CpuCount=2
#Ram Size
$RAMCount=1GB
#VMName , will also become the Computer Name
$Name="Test"
#IP Address
$IPDomain="192.168.0.1"
#Default Gateway to be used
$DefaultGW="192.168.0.254"
#DNS Server
$DNSServer="192.168.0.1"
#DNS Domain Name
$DNSDomain="test.com"
#Hyper V Switch Name
$SwitchNameDomain="HyperV"
#Set the VM Domain access NIC name
$NetworkAdapterName="Public"
#User name and Password
$AdminAccount="Administrator"
$AdminPassword="P@ssw0rd"
#Org info
$Organization="Test Organization"
#This ProductID is actually the AVMA key provided by MS
$ProductID="TMJ3Y-NTRTM-FJYXT-T22BY-CWG3J"
#Where's the VM Default location? You can also specify it manually
$Path= Get-VMHost |select VirtualMachinePath -ExpandProperty VirtualMachinePath
#Where should I store the VM VHD?, you actually have nothing to do here unless you want a custom name on the VHD
$VHDPath=$Path + $Name + "\" + $Name + ".vhdx"
#Where are the folders with prereq software ?
$StartupFolder="C:\ISO"
$TemplateLocation="C:\ISO\Template2016.vhdx"
$UnattendLocation="C:\ISO\unattend.xml"
 
#Part 1 Complete-------------------------------------------------------------------------------#
 
#Part 2 Initialize---------------------------------------------------------------------------------#
#Start the Party!
#Let's see if there are any VM's with the same name if you actually find any simply inform the user
$VMS=Get-VM
Foreach($VM in $VMS)
{
 if ($Name -match $VM.Name)
 {
 write-host -ForegroundColor Red "Found VM With the same name!!!!!"
 $Found=$True
 }
}
 
#Create the VM
New-VM -Name $Name -Path $Path  -MemoryStartupBytes $RAMCount  -Generation 2 -NoVHD
 
#Remove any auto generated adapters and add new ones with correct names for Consistent Device Naming
Get-VMNetworkAdapter -VMName $Name |Remove-VMNetworkAdapter
Add-VMNetworkAdapter -VMName $Name -SwitchName $SwitchNameDomain -Name $NetworkAdapterName -DeviceNaming On
 
#Start and stop VM to get mac address, then arm the new MAC address on the NIC itself
start-vm $Name
sleep 5
stop-vm $Name -Force
sleep 5
$MACAddress=get-VMNetworkAdapter -VMName $Name -Name $NetworkAdapterName|select MacAddress -ExpandProperty MacAddress
$MACAddress=($MACAddress -replace '(..)','$1-').trim('-')
get-VMNetworkAdapter -VMName $Name -Name $NetworkAdapterName|Set-VMNetworkAdapter -StaticMacAddress $MACAddress
 
#Copy the template and add the disk on the VM. Also configure CPU and start - stop settings
Copy-item $TemplateLocation -Destination  $VHDPath
Set-VM -Name $Name -ProcessorCount $CpuCount  -AutomaticStartAction Start -AutomaticStopAction ShutDown -AutomaticStartDelay 5 
Add-VMHardDiskDrive -VMName $Name -ControllerType SCSI -Path $VHDPath
 
#Set first boot device to the disk we attached
$Drive=Get-VMHardDiskDrive -VMName $Name | where {$_.Path -eq "$VHDPath"}
Get-VMFirmware -VMName $Name | Set-VMFirmware -FirstBootDevice $Drive
 
#Prepare the unattend.xml file to send out, simply copy to a new file and replace values
Copy-Item $UnattendLocation $StartupFolder\"unattend"$Name".xml"
$DefaultXML=$StartupFolder+ "\unattend"+$Name+".xml"
$NewXML=$StartupFolder + "\unattend$Name.xml"
$DefaultXML=Get-Content $DefaultXML
$DefaultXML  | Foreach-Object {
 $_ -replace '1AdminAccount', $AdminAccount `
 -replace '1Organization', $Organization `
 -replace '1Name', $Name `
 -replace '1ProductID', $ProductID`
 -replace '1MacAddressDomain',$MACAddress `
 -replace '1DefaultGW', $DefaultGW `
 -replace '1DNSServer', $DNSServer `
 -replace '1DNSDomain', $DNSDomain `
 -replace '1AdminPassword', $AdminPassword `
 -replace '1IPDomain', $IPDomain `
 } | Set-Content $NewXML
 
#Mount the new virtual machine VHD
mount-vhd -Path $VHDPath
#Find the drive letter of the mounted VHD
$VolumeDriveLetter=GET-DISKIMAGE $VHDPath | GET-DISK | GET-PARTITION |get-volume |?{$_.FileSystemLabel -ne "Recovery"}|select DriveLetter -ExpandProperty DriveLetter
#Construct the drive letter of the mounted VHD Drive
$DriveLetter="$VolumeDriveLetter"+":"
#Copy the unattend.xml to the drive
Copy-Item $NewXML $DriveLetter\unattend.xml
#Dismount the VHD
Dismount-Vhd -Path $VHDPath
#Fire up the VM
Start-VM $Name
#Part 2 Complete---------------------------------------------------------------------#