During the MDT Gather process, almost all properties (variables) are treated as “first writer wins” (i.e. the value cannot be changed during Gather once it is set for the first time). ZTIGather.xml defines the “known” MDT variables for Gather. The ones that are able to be changed (overwritten) during Gather have the property overwrite="true". Any custom variable not defined in ZTIGather.xml is treated as having overwrite="false".
This behavior may not be desirable in all cases. For example, for one deployment I was setting a custom variable during Gather called ComputerRecordExists that would be True if an MDT database record existed for the computer and False if it did not. In this task sequence, if ComputerRecordExists was found to be False, then a stored procedure was run against the scheduling database. If the computer was scheduled for deployment, the procedure would kick off a server side process to create the database record. I then needed to run Gather a second time and reset ComputerRecordExists (and a few other variables) based on if a record now existed. However, by default the second Gather would not change the values because they had already been set. There are a few ways this could be handled with existing task sequence steps or configuration files but none were particularly elegant to me.
First, I could have added static Set Task Sequence Variable steps to clear the values of all the variables in questions. I wasn’t happy with having to have step external to Gather for this. Second, variables can be added to ZTIGather.xml and variables in there can have their overwrite property changed. However, doing this creates a persistent change for that Deployment Share/Toolkit package. Also, changes to ZTIGather.xml may also be overwritten when upgrading versions of MDT. What I really wanted was just to have a directive in CustomSettings.ini that would make the specified variables over-writable for that run of Gather.
So I took a deep dive into ZTIGather.wsf and ZTIUtility.vbs and managed to accomplish just this. The route to getting there is kind of complicated. If you’re not interested in the internals of these scripts and the code I used to do this, just jump down to the implementation section below. For those who are interested, here we go.
Starting with BDD 2007, the deployment scripts in MDT were written as VBScripts in the Windows Script File (WSF) format used by the Windows Script Host. The original ones (BDD 2007 – MDT 2008) used a main ZTIProcess function with the other supporting functions in global (or script) scope. (If you are not familiar with the concept of scope in VBScript, there is an overview here: https://msdn.microsoft.com/en-us/subscriptions/t7zd6etz(v=vs.84).aspx.)
Starting with MDT 2010 the WSF scripts were changed to implement almost all the code in a VBScript Class. The Class name must match script file name. This was done to better facilitate unit testing. Because of this change, the procedures/variables in the scripts are no longer available in global scope. Why is not having the procedures/variables available in global scope an issue? Because you may want to do something like calling functions or access objects in the ZTIGather.wsf class in CustomSetting.iniand this will not work the way these script are written.
In the ZTIGather.wsf class, the list of properties for which overwrite="true" are stored in a Dictionary object called dicOverwrite. Because this object is not available in the global scope we cannot simply add a line like AddToOverwiteDic=#dicOverwrite.Add("ComputerRecordExists", "")# to CustomSettings.inito add a property to the list
Fortunately, while digging though ZTIUtility.vbs I found the place where the MDT Team hooks in to run their unit test code. (You learn something new every day.) This can be leveraged to run custom script code to solve this problem. To understand this, I have to give you a little insight into what I found out about what happens when an MDT 2010-style WSF script is run. The first thing the script does is call RunNewInstance. This is a global function in ZTIUtility.vbs. RunNewInstance calls oUtility.RunNewInstanceEx. This function actually creates the instance of the script Class as the variable oScriptClass in the scope of this function. After that it looks to see if the /TestHook command line argument was specified. If it was, the VBScript file specified with the /TestHook argument is read and executed in the same local scope. Finally the Main function in the class is run in the same scope. Because this TestHook file is executed in the same scope as the oScriptClassobject, the code in the TestHook file can then make it available in the global scope using these lines of VBScript:
ExecuteGlobal "Dim oScriptClassGlobal"
Set oScriptClassGlobal = oScriptClass
These lines dimension the oScriptClassGlobal variable in the global scope and then set it equal to the oScriptClass object. Now oScriptClassGlobalcan be used to access the properties and method in the script Class instance in the global scope.
The TestHook script that I created, MDTTestHookGatherOverwriteProperties.vbs, takes this a little further. When RunNewInstanceEx locally executes the script, the code above is run and a global variable called bTestHookExecuteLocal is set to True indicate that this first execution happened. Then the TestHook script reads the contents of itself and executes it in the global scope to make a function called AddOverwriteProperties available globally. The global variable bTestHookExecuteLocal is checked to make sure the second global execution does not redo the tasks that only needed to be done in the first local execution. The AddOverwriteProperties function is the function that will be used to dynamically make variables over-writable in CustomSettings.ini.
Implementing the Solution
Fortunately, implementing this is simpler than understanding how it works. First, add MDTTestHookGatherOverwriteProperties.vbs to Scripts folder in the LTI Deployment Share or ConfigMgr MDT Toolkit package.
Because of the way the TestHook works, you cannot use the MDT Gather task sequence step since this process requires passing the /TestHook argument on the ZTIGather.wsf command line. So for any Gather where you want to use this technique, you will need to use a Run Command Line step instead. The command line for this step should be
cscript "%DeployRoot%\Scripts\ZTIGather.wsf" /inifile:CustomSettings.ini /TestHook:"%DeployRoot%\Scripts\MDTTestHookGatherOverwriteProperties.vbs"
You would then change CustomSettings.ini to add the highlighted items below:
Priority=TestHookSubsection, TestCustomProperty1, TestCustomProperty2, Default
The AddOverwriteProperties function takes as a parameter a string that is a comma separated list of properties to be added to the list of properties that can be overwritten. This should work for standard and custom properties. Caution should be take when changing the behavior of standard MDT properties as the MDT scripts may not expect these to change. Test thoroughly before doing that.
Without using MDTTestHookGatherOverwriteProperties.vbs as the TestHook, the custom property MyCustomProperty in the example would retain the value of Foo even when the TestCustomProperty2 section is processed. With this TestHook script in place, the value will change to Bar when the TestCustomProperty2 section is processed.
MDTTestHookGatherOverwriteProperties.vbs is provided below.
This post was contributed by Michael Murgolo, a Senior Consultant with Microsoft Services - U.S. East Region.