Dynamically Updating Unattend.XML during an OSD Task Sequence using MDT Variables and ZTI Scripts.

While trying to set the KeyboardLocale in my ConfigMgr/SCCM Task Sequence, I found that my settings weren’t getting updated during the Task Sequence. I used to think that custom Unattend.XML that you include in your Task Sequence would be used as-is. I was wrong. I did a bit of digging and here’s what I discovered.

FYI: I’m using ConfigMgr/SCCM with MDT integrated and I’m using an MDT Task Sequence built in ConfigMgr as the basis for this discussion. Depending on your setup, your mileage may vary.

During some early testing with my Windows 10 Task Sequence, I was adding sections to my Unattend.XML then finding that they were getting removed during the Task Sequence (See a previous post about Modern Driver Management). When you add the Apply Operating System, Apply Windows Settings or Apply Network Settings steps (among others) to your Task Sequence, everything in those steps is used later by the Setup Windows and Configuration Manager step in the form of command line arguments or updates to the configuration files such as Unattend.XML. If you start with a custom Unattend.XML file then have a conflicting entry in one of your steps, the Task Sequence step entries will overwrite the custom entry in your Unattend.XML file.

In addition to the native Task Sequence steps updating the your config files, MDT has a set of scripts that do all sorts of cool things. In my case, I have a custom UDI wizard that captures information and sets them as Task Sequence variables - you can also do this by using CustomSettings.INI or several other methods to set these variables - the end result being a set of variables that allow me to dynamically customize the image (TimeZone, KeyboardLocale, SiteCode, ComputerName, etc). Johan has some a short write up on how to create custom MDT variables here. By using MDT built-in variable names in CustomSettings.INI or in your script, the ZTI scripts will be able to pick these variables up and map them to the right places automatically. You can view the list of variables and mappings in the MDT Toolkit Under the Scripts folder (ZTIGather.xml and ZTIConfigure.xml).

Going into this, I knew about ZTIGather and the MDT variable names but honestly just thought ZTIGather was taking care of consuming the variables until I found this issue. As we’ve been testing our Windows 10 task sequence, one of my colleagues from Belgium called and said the image was working, but their keyboard layout wasn’t being set to Dutch (we leave the OS language set to English, just change the keyboard). I checked in our database and scripts and verified that we had the right locale and the KeyboardLocale variable was being populated properly. Then I checked the BDD.log and found the missing link. The BDD log compiles several ZTI logs into one, including ZTIGather and ZTIConfigure. What I found was the InputLocale and KeyboardLocale were being skipped by ZTIConfigure.

1
2
<![LOG[//unattend:settings[@pass="oobeSystem"]/unattend:component[@name="Microsoft-Windows-International-Core"]/unattend:InputLocale not found in C:\WINDOWS\panther\unattend\unattend.xml, unable to update.]LOG]!><time="06:52:51.000+000" date="07-10-2018" component="zticonfigure" context="" type="1" thread="" file="zticonfigure">
<![LOG[//unattend:settings[@pass="specialize"]/unattend:component[@name="Microsoft-Windows-International-Core"]/unattend:SystemLocale not found in C:\WINDOWS\panther\unattend\unattend.xml, unable to update.]LOG]!><time="06:52:51.000+000" date="07-10-2018" component="zticonfigure" context="" type="1" thread="" file="zticonfigure">

I took a look at the ZTIConfigure.XML and found this section:

1
2
3
4
<mapping id="KeyboardLocale" type="xml">
           <xpath removeIfBlank="Self"><![CDATA[//unattend:settings[@pass="specialize"]/unattend:component[@name="Microsoft-Windows-International-Core"]/unattend:InputLocale]]></xpath>
           <xpath removeIfBlank="Self"><![CDATA[//unattend:settings[@pass="oobeSystem"]/unattend:component[@name="Microsoft-Windows-International-Core"]/unattend:InputLocale]]></xpath>
</mapping>

It shows me that if ZTIConfigure finds a variable named KeyboardLocale it will write the value of that variable into 2 locations inside Unattend.XML. If my KeyboardLocale variable was set to 0813:00000813 during the Task Sequence to change the default keyboard to Dutch for our Belgium office, then ZTIConfigure would look for specialize and oobeSystem sections in the Unattend.XML and update the InputLocale property to 0813:00000813 with the resulting entry being (only InputLocale would change, showing the whole section for context):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <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>0813:00000813</InputLocale>
            <SystemLocale>en-US</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UILanguageFallback>en-US</UILanguageFallback>
            <UserLocale>en-US</UserLocale>
        </component>
    </settings>
    </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>0813:00000813</InputLocale>
            <SystemLocale>en-US</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UILanguageFallback>en-US</UILanguageFallback>
            <UserLocale>en-US</UserLocale>
        </component>
    </settings>
</unattend>

The resulting output in the BDD.log will be something like this:

1
2
<![LOG[Updated C:\WINDOWS\panther\unattend\unattend.xml with KeyboardLocale=0813:00000813 (value was 0409:00000409)]LOG]!><time="07:38:13.000+000" date="07-11-2018" component="zticonfigure" context="" type="1" thread="" file="zticonfigure">
<![LOG[Updated C:\WINDOWS\panther\unattend\unattend.xml with KeyboardLocale=0813:00000813 (value was 0409:00000409)]LOG]!><time="07:38:13.000+000" date="07-11-2018" component="zticonfigure" context="" type="1" thread="" file="zticonfigure">

Here’s the part that I was missing. If my custom Unattend.XML file that I specified in my Task Sequence doesn’t already have a default entry, the above steps get skipped. After digging through the ZTIConfigure script, I was able to verify that the script will skip any variables (well not all, there are defaults that always get mapped) that don’t exist in your Unattend.XML. So the morale of the story is, if you are using ZTIGather and ZTIConfigure in your Task Sequence, be sure to check out the mapping that the scripts perform and ensure you have placeholder entries in your Unattend.XML or the entries won’t get updated.