Windows Image Tools | Unattend.xml

5 minute read

Getting started

The second item need to automate the creation of a VM is a way to bootstrap said automation of a fresh windows image. That requires an unattend.xml

prerequisites

Now to work with Unattent.xml you need a deep understanding of XML structure and namespace… Just kidding. I’m going to use New-UnattendXml part of WindowsImageTools

You can install WindowsImageTools from the PowerShell Gallery with install-module WindowsImageTools

Creating a basic Unattent.xml

Lets start off with a basic Unattend. Minimum requirements for this is changing the administrator password

New-UnattendXml -AdminCredential (Get-Credential) |
Get-Content
cmdlet Get-Credential at command pipeline position 1
Supply values for the following parameters:
Credential
WARNING: C:\Users\BLADE_~1\AppData\Local\Temp\hqfyjyg2\unattend.xml only usable on a server SKU, for a client OS, use
either -EnableAdministrator or -UserAccount

Notice the warning. Setting the Admin password on a server is all you need, as the build in Administrator account is allready enabled. On a client OS you have to either enable the administrator or add a second one or three. It spits out this mass of XML

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
  <settings pass="specialize">
    <component name="Microsoft-Windows-Deployment" 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">
    </component>
    <component name="Microsoft-Windows-Deployment" 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">
    </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>*</ComputerName>
    </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>*</ComputerName>
    </component>
  </settings>
  <settings pass="oobeSystem">
    <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>en-US</InputLocale>
      <SystemLocale>en-US</SystemLocale>
      <UILanguage>en-US</UILanguage>
      <UserLocale>en-US</UserLocale>
    </component>
    <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>en-US</InputLocale>
      <SystemLocale>en-US</SystemLocale>
      <UILanguage>en-US</UILanguage>
      <UserLocale>en-US</UserLocale>
    </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">
      <OOBE>
        <HideEULAPage>true</HideEULAPage>
        <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
        <NetworkLocation>Work</NetworkLocation>
        <ProtectYourPC>1</ProtectYourPC>
        <SkipUserOOBE>true</SkipUserOOBE>
        <SkipMachineOOBE>true</SkipMachineOOBE>
      </OOBE>
      <TimeZone>GMT Standard Time</TimeZone>
      <UserAccounts>
        <AdministratorPassword>
          <Value>YQBkAHMAZgBBAGQAbQBpAG4AaQBzAHQAcgBhAHQAbwByAFAAYQBzAHMAdwBvAHIAZAA=</Value>
          <PlainText>false</PlainText>
        </AdministratorPassword>
      </UserAccounts>
      <RegisteredOrganization>Generic Organization</RegisteredOrganization>
      <RegisteredOwner>Generic Owner</RegisteredOwner>
    </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">
      <OOBE>
        <HideEULAPage>true</HideEULAPage>
        <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
        <NetworkLocation>Work</NetworkLocation>
        <ProtectYourPC>1</ProtectYourPC>
        <SkipUserOOBE>true</SkipUserOOBE>
        <SkipMachineOOBE>true</SkipMachineOOBE>
      </OOBE>
      <TimeZone>GMT Standard Time</TimeZone>
      <UserAccounts>
        <AdministratorPassword>
          <Value>YQBkAHMAZgBBAGQAbQBpAG4AaQBzAHQAcgBhAHQAbwByAFAAYQBzAHMAdwBvAHIAZAA=</Value>
          <PlainText>false</PlainText>
        </AdministratorPassword>
      </UserAccounts>
      <RegisteredOrganization>Generic Organization</RegisteredOrganization>
      <RegisteredOwner>Generic Owner</RegisteredOwner>
    </component>
  </settings>
</unattend>

Anatomy of Unattent.xml

Takeing a closer look at each section

Specialize : Deployment

Contrary to popular belief, the unattend.xml can have one file that can be use for both 32 and 64bit. All you have to do is repeat the section with a different architecture. With one caveat that I will get into later. In the basic Unattent the Deployment section is empty.

<component name="Microsoft-Windows-Deployment" 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">
</component>
<component name="Microsoft-Windows-Deployment" 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">
</component>

Specialize : Shell Setup

Basic settings lets windows pick a random computer name.

<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>*</ComputerName>
</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>*</ComputerName>
</component>

oobeSystem : International Core

The language and input are localized to en-US by default, This can be changed via parameters

<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>en-US</InputLocale>
  <SystemLocale>en-US</SystemLocale>
  <UILanguage>en-US</UILanguage>
  <UserLocale>en-US</UserLocale>
</component>
<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>en-US</InputLocale>
  <SystemLocale>en-US</SystemLocale>
  <UILanguage>en-US</UILanguage>
  <UserLocale>en-US</UserLocale>
</component>

oobeSystem : Shell-Setup

This section is a bit larger. It going to hide or skip everything it can to avoid any prompting. Afterall this is suposed to be automated. We also set the Timezone to a default of GMT and set the admin password Now the Admin Password may look encrypted, but it’s not that is 64bit encodeing. The best thing to do is set a password here for troubleshooting, then change it via LAPS or DSC once windows is up and running.

<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>
    <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
    <NetworkLocation>Work</NetworkLocation>
    <ProtectYourPC>1</ProtectYourPC>
    <SkipUserOOBE>true</SkipUserOOBE>
    <SkipMachineOOBE>true</SkipMachineOOBE>
  </OOBE>
  <TimeZone>GMT Standard Time</TimeZone>
  <UserAccounts>
    <AdministratorPassword>
      <Value>YQBkAHMAZgBBAGQAbQBpAG4AaQBzAHQAcgBhAHQAbwByAFAAYQBzAHMAdwBvAHIAZAA=</Value>
      <PlainText>false</PlainText>
    </AdministratorPassword>
  </UserAccounts>
  <RegisteredOrganization>Generic Organization</RegisteredOrganization>
  <RegisteredOwner>Generic Owner</RegisteredOwner>
</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">
  <OOBE>
    <HideEULAPage>true</HideEULAPage>
    <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
    <NetworkLocation>Work</NetworkLocation>
    <ProtectYourPC>1</ProtectYourPC>
    <SkipUserOOBE>true</SkipUserOOBE>
    <SkipMachineOOBE>true</SkipMachineOOBE>
  </OOBE>
  <TimeZone>GMT Standard Time</TimeZone>
  <UserAccounts>
    <AdministratorPassword>
      <Value>YQBkAHMAZgBBAGQAbQBpAG4AaQBzAHQAcgBhAHQAbwByAFAAYQBzAHMAdwBvAHIAZAA=</Value>
      <PlainText>false</PlainText>
    </AdministratorPassword>
  </UserAccounts>
  <RegisteredOrganization>Generic Organization</RegisteredOrganization>
  <RegisteredOwner>Generic Owner</RegisteredOwner>
</component>

This is all well and good but it’s still only get’s us the first step. To automate deployment we need to do more then bypass prompts and set a password.

More usefull Unattent.xml

Now we will set a few more items.

$param = @{
  AdminCredential = (Get-Credential) 
  UserAccount = (Get-Credential), (Get-Credential) 
  FirstBootScriptPath = 'C:\PsTemp\FirstBoot.ps1' 
  TimeZone = 'Central Standard Time' 
  RegisteredOwner = 'Employee'
  RegisteredOrganization = 'Contoso'
  enableAdministrator = $true 
}
New-UnattendXml @param | 
get-content

specialize : Deployment

One thing to notice here is the amd64 component is still blank. The reason for this is 64bit OS’s will run the commands in both the 32 and 64bit sections.

We have two commands in the RunSynchronous section. The first one enables the Administrator account. the second launches PowerShell with the script path we asked for.

<component name="Microsoft-Windows-Deployment" 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">
</component>
<component name="Microsoft-Windows-Deployment" 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">
  <RunSynchronous>
    <RunSynchronousCommand wcm:action="add">
      <Description>Enable Administrator</Description>
      <Order>1</Order>
      <Path>net user administrator /active:yes</Path>
    </RunSynchronousCommand>
    <RunSynchronousCommand wcm:action="add">
      <Description>PowerShell First boot script</Description>
      <Order>2</Order>
      <Path>%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\PsTemp\FirstBoot.ps1"</Path>
    </RunSynchronousCommand>
  </RunSynchronous>
</component>

oobeSystem : Shell Setup

The next section that is diferent is the shell setup. You will notice I set two additional user accounts. Remember that the passwords are encoded not encrypted. I also have entries to set the ownership.

<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>
    <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
    <NetworkLocation>Work</NetworkLocation>
    <ProtectYourPC>1</ProtectYourPC>
    <SkipUserOOBE>true</SkipUserOOBE>
    <SkipMachineOOBE>true</SkipMachineOOBE>
  </OOBE>
  <TimeZone>Central Standard Time</TimeZone>
  <UserAccounts>
    <AdministratorPassword>
      <Value>UABAAHMAcwB3ADAAcgBkAEEAZABtAGkAbgBpAHMAdAByAGEAdABvAHIAUABhAHMAcwB3AG8AcgBkAA==</Value>
      <PlainText>false</PlainText>
    </AdministratorPassword>
    <LocalAccounts>
      <LocalAccount wcm:action="add">
        <Password>
          <Value>UABAAHMAcwB3ADAAcgBkADEAMgAzAFAAYQBzAHMAdwBvAHIAZAA=</Value>
          <PlainText>false</PlainText>
        </Password>
        <DisplayName>Don</DisplayName>
        <Group>Administrators</Group>
        <Name>Don</Name>
      </LocalAccount>
      <LocalAccount wcm:action="add">
        <Password>
          <Value>UABAAHMAcwB3ADAAcgBkADQANQA2AFAAYQBzAHMAdwBvAHIAZAA=</Value>
          <PlainText>false</PlainText>
        </Password>
        <DisplayName>Mike</DisplayName>
        <Group>Administrators</Group>
        <Name>Mike</Name>
      </LocalAccount>
    </LocalAccounts>
  </UserAccounts>
  <RegisteredOrganization>Contoso</RegisteredOrganization>
  <RegisteredOwner>Employee</RegisteredOwner>
</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">
  <OOBE>
    <HideEULAPage>true</HideEULAPage>
    <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
    <NetworkLocation>Work</NetworkLocation>
    <ProtectYourPC>1</ProtectYourPC>
    <SkipUserOOBE>true</SkipUserOOBE>
    <SkipMachineOOBE>true</SkipMachineOOBE>
  </OOBE>
  <TimeZone>Central Standard Time</TimeZone>
  <UserAccounts>
    <AdministratorPassword>
      <Value>UABAAHMAcwB3ADAAcgBkAEEAZABtAGkAbgBpAHMAdAByAGEAdABvAHIAUABhAHMAcwB3AG8AcgBkAA==</Value>
      <PlainText>false</PlainText>
    </AdministratorPassword>
    <LocalAccounts>
      <LocalAccount wcm:action="add">
        <Password>
          <Value>UABAAHMAcwB3ADAAcgBkADEAMgAzAFAAYQBzAHMAdwBvAHIAZAA=</Value>
          <PlainText>false</PlainText>
        </Password>
        <DisplayName>Don</DisplayName>
        <Group>Administrators</Group>
        <Name>Don</Name>
      </LocalAccount>
      <LocalAccount wcm:action="add">
        <Password>
          <Value>UABAAHMAcwB3ADAAcgBkADQANQA2AFAAYQBzAHMAdwBvAHIAZAA=</Value>
          <PlainText>false</PlainText>
        </Password>
        <DisplayName>Mike</DisplayName>
        <Group>Administrators</Group>
        <Name>Mike</Name>
      </LocalAccount>
    </LocalAccounts>
  </UserAccounts>
  <RegisteredOrganization>Contoso</RegisteredOrganization>
  <RegisteredOwner>Employee</RegisteredOwner>
  <FirstLogonCommands />
  <LogonCommands />
</component>

Puting it together

New-UnattentXML returns a path object to the file created, and by default that is in $env:TEMP. so we will store the path in a variable and use it pluss the path to our FirstBoot script when creating the VHDX

Adding the unattend and script file when creating a VHDX

$UnattentParam = @{
  AdminCredential = (Get-Credential) 
  UserAccount = (Get-Credential), (Get-Credential) 
  FirstBootScriptPath = 'C:\PsTemp\FirstBoot.ps1' 
  TimeZone = 'Central Standard Time' 
  RegisteredOwner = 'Employee'
  RegisteredOrganization = 'Contoso'
  enableAdministrator = $true 
}
$UnattentPath = (New-UnattendXml @UnattentParam ).FullName

$FirstBootContent = {
  ## Do something Cool
}
 New-Item -Path "G:\filesToInject\PsTemp" -Name FirstBoot.ps1 -ItemType 'file' -Value $FirstBootContent
$ConverParm = @{
  Path = 'G:\vhd\2012r2_eval_Core.vhdx' 
  Size = 60gb 
  Dynamic = $true 
  DiskLayout = 'UEFI' 
  SourcePath = 'G:\iso\Srv2012r2Eval.ISO' 
  Index = 1 
  Feature = 'NetFx3'
  Unattend = $UnattentPath 
  filesToInject = 'G:\filesToInject\PsTemp'
}
 Convert-Wim2VHD @ConverParm -Verbose

Below is part of the verbose output that covers Unattend and filesToInject

VERBOSE: [Set-VHDPartition] [2012r2_eval_Core.vhdx] Windows Partition [3] : Adding files from G:\filesToInject
VERBOSE: [Set-VHDPartition] [2012r2_eval_Core.vhdx] Windows Partition [3] : Adding Unattend.xml (C:\Users\Blade_000\AppData\Local\Temp\djebkgrz\unattend.xml)

So now when this vhdx first boots it will process Unattend creating users and running our PowerShell script

Leave a Comment

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

Loading...