Publishing ZTICustomConfiguration.wsf – a script to solve some common tasks in MDT Deployments
There are a couple things, that are quite common in most deployments. Very often you need to delete a couple files, copy some folders, remove or update some registry keys, maybe update a XML or INI file etc. What a lot of people typically do is to write some kind of cleanup-script that runs at the end of their task sequence and removes unnecessary shortcuts, updates the registry to disable some auto-update features or whatever. Group Policy Preferences or Group Policies would probably often be a better choice for most of the scenarios, but lets skip them for a moment.
What I would like to publish today is a generic script, that covers all these common tasks, and allows you to configure them in a simple to use xml file. It ties nicely into MDT with all their logging and error handling etc. Lets have a quick run-through on what it can do, and then look at some samples on how to use it.
What can it do?
- Create, copy, move and delete files and folders
- Create, update, delete and read(!) registry settings including mounting of offline hives
- Create, update, delete and read XML, INI and simple text files
- Use MDT properties, environment variables and even VBScript functions as values, for path or file names, etc.
- restrict execution based on rules
How to use it?
- Download the script and xml file from CodePlex
- Drop them in the MDT scripts folder, or add them to your MDT scripts package source folder in ConfigMgr
- Update the ZTICustomConfiguration.xml file according to your specific needs (see samples section in the file or this blog post for more information)
- Test your changes (will publish a blog post about how to test this script without a full deployment pretty soon)
- Optionally create a new list property within MDT and add all configuration sets, that you would like to have processed (see sample customsettings.ini in the download)
- Add a “Run command line” step to your Task Sequence and have it execute “cscript.exe %ScriptRoot%ZTICustomConfiguration.wsf”
- Refresh the Deployment Share / Update the ConfigMgr package
- Execute the Task Sequence
The configuration file
The execution of this script is driven by a xml file called “ZTICustomConfiguration.xml”. It should be placed in the MDT scripts folder, together with the script file ZTICustomConfiguration.wsf. This xml file can contain several so called “configuration sets”. And you can tell the script what set(s) shall be processed and in which order. So you could have one common configuration file for all your Task Sequences, but just process some of these configurations depending on the Task Sequence or other prereqs. There is one “Default” configuration set, that will be processed if nothing else is configured.
The Rules
All actions at any level, from the whole configuration set down to an individual action, can be bound to rules. However, there is no necessity to specify rules, in this case everything will just be processed. Rules can be used to either include and/or exclude computers from processing something. And by combining those include and exclude rules, you will be able to cover even pretty complex scenarios. All rules rely on the evaluation of MDT properties. So mainly, if they contain a specific value, if they are empty, etc. We will see some samples in a minute. It might sound a bit limiting, but there are a couple hundred predefined MDT properties, ready to use and you can still define your own. And as the script allows you to read e.g. keys/values from the registry or a file into a MDT property, actually almost any information from a computer can be used for the evaluation.
As a sample, to e.g. run a certain set of actions only for Win 7 64 Bit, you could specify the following Rules:
1 2 3 4 5 6 |
<rules> <include condition="all"> <rule property="OSVersion" operator="equals" value="Win7Client" /> <rule property="Architecture" operator="equals" value="X64" /> </include> </rules> |
We could write the same by excluding the x86 architecture
1 2 3 4 5 6 7 8 |
<rules> <include condition="any"> <rule property="OSVersion" operator="equals" value="Win7Client" /> </include> <exclude condition="any"> <rule property="Architecture" operator="equals" value="X86" /> </exclude> </rules> |
I guess you get the point.
Currently the script supports the following operators (see the ZTICustomConfiguration.xml file for complete Syntax)
- equals
- startswith
- endswith
- contains
- notcontains
- empty
- notempty
Registry operations
Let’s start with some often used necessity and update the registry. As you might be aware, on default, a normal User, even a PowerUser can’t install Printer drivers on Windows 7 64 Bit. Yes, there is a GroupPolicy, but hence, we want to fix this permanently during our deployment. This is a sample on what we would need to configure, to get this done:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!-- Allow users to install drivers for the printers/scanners on Windows 7 64 Bit --> <registry> <rules> <include condition="all"> <rule property="OSVersion" operator="equals" value="Win7Client" /> <rule property="Architecture" operator="equals" value="X64" /> </include> </rules> <!-- Allow Devices Classes--> <key path="HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\DriverInstall\Restrictions\AllowUserDeviceClasses" operation="update" type="REG_DWORD">00000001</key> <key path="HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\DriverInstall\Restrictions\AllowUserDeviceClasses\1" operation="update" type="REG_SZ">{6BDD1FC6-810F-11D0-BEC7-08002BE2092F}</key> <key path="HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\DriverInstall\Restrictions\AllowUserDeviceClasses\2" operation="update" type="REG_SZ">{48721B56-6795-11D2-B1A8-0080C72E74A2}</key> <key path="HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\DriverInstall\Restrictions\AllowUserDeviceClasses\3" operation="update" type="REG_SZ">{49CE6AC8-6F86-11D2-B1E5-0080C72E74A2}</key> <key path="HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\DriverInstall\Restrictions\AllowUserDeviceClasses\4" operation="update" type="REG_SZ">{4658EE7E-F050-11D1-B6BD-00C04FA372A7}</key> <key path="HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\DriverInstall\Restrictions\AllowUserDeviceClasses\5" operation="update" type="REG_SZ">{4D36E971-E325-11CE-BFC1-08002BE10318}</key> <key path="HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\DriverInstall\Restrictions\AllowUserDeviceClasses\6" operation="update" type="REG_SZ">{4D36E979-E325-11CE-BFC1-08002BE10318}</key> <!-- Switch off PointAndPrint restriction --> <key path="HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Printers\PointAndPrint\Restricted" operation="update" type="REG_DWORD">00000000</key> <key path="HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Printers\PointAndPrint\InForest" operation="update" type="REG_DWORD">00000000</key> <key path="HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Printers\PointAndPrint\TrustedServers" operation="update" type="REG_DWORD">00000000</key> </registry> |
The same way, we created/updated a registry key or value, we can also delete one. But how about reading a value and storing it in a MDT property so it can be used later? Le’s say we want to read the BIOSVersion from the registry into a custom MDT property called “MyBIOSVersion”.
1 2 3 |
<registry> <key path="HKLM\SYSTEM\CurrentControlSet\Control\SystemInformation\BIOSVersion" operation="read" property="MyBIOSVersion" /> </registry> |
That’s it. Now we could use this value to process some other rules etc. But the values and names we use, don’t need to be static. At almost any place we can make use of environment variables, MDT properties, even vbscript functions if we want to. Let’s tattoo some deployment info to the registry:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<registry> <rules> <exclude condition="any"> <!-- tattoo only in the full OS --> <rule property="OSVersion" operator="equals" value="WinPE" /> </exclude> </rules> <key path="HKLM\Software\Microsoft\Deployment 4\Deployment Method" operation="update" type="REG_SZ">%DeploymentMethod%</key> <key path="HKLM\Software\Microsoft\Deployment 4\Deployment Type" operation="update" type="REG_SZ">%DeploymentType%</key> <key path="HKLM\Software\Microsoft\Deployment 4\Task Sequence ID" operation="update" type="REG_SZ">%TaskSequenceID%</key> <key path="HKLM\Software\Microsoft\Deployment 4\Task Sequence Name" operation="update" type="REG_SZ">%TaskSequenceName%</key> <key path="HKLM\Software\Microsoft\Deployment 4\Task Sequence Version" operation="update" type="REG_SZ">%TaskSequenceVersion%</key> <key path="HKLM\Software\Microsoft\Deployment 4\Demo" operation="update" type="REG_SZ">#Left("%OSVersion%",3)#</key> </registry> |
As you can see, we use a couple MDT properties and store their values in the Registry. In the last key, we store just the first three characters from the “OSVersion” property in the registry. Again, I guess you can imagine, that there will be a lot more advanced functions, that can be used with. This is not limited to values, you can use them in the path also.
The final thing I want to show on registry operations, is the possibility to mount a hive first, before updating it. That’s a pretty common task if you want to make some changes on the default profile. Let’s see how we can load the default user registry and disable Desktop Cleanup
1 2 3 4 5 6 |
<!-- Disable Desktop Cleanup in default User profile --> <registry> <hive source="%USERPROFILE%\..\Default User\NTUSER.DAT"> <key path="Software\Microsoft\Windows\CurrentVersion\Explorer\Desktop\CleanupWiz\NoRun" operation="update" type="REG_DWORD">1</key> </hive> </registry> |
The script will take care about loading and unloading the hive and redirecting the keys.
File and Folder operations
The next pretty common thing is executing some file and/or folder operation like moving a folder from a network share to the local computer, copying some files to the default/administrator profile, removing some unwanted shortcuts. etc. Let’s create, copy, move and delete some files and folders
1 2 3 4 5 6 7 8 9 |
<folder source="%Temp%\CustomConfigurationDemo" operation="create" /> <folder source="%Temp%\CustomConfigurationDemo\FolderCopyTest" operation="create" /> <folder source="%Temp%\CustomConfigurationDemo\FolderCopyTest" target="%Temp%\CustomConfigurationDemo\FolderMoveTest" operation="copy" /> <folder source="%Temp%\CustomConfigurationDemo\FolderMoveTest" target="%Temp%\CustomConfigurationDemo\FolderDeleteTest" operation="move" /> <folder source="%Temp%\CustomConfigurationDemo\FolderDeleteTest" operation="delete" /> <file source="%Temp%\CustomConfigurationDemo\FileCopyTest.txt" operation="create" /> <file source="%Temp%\CustomConfigurationDemo\FileCopyTest.txt" target="%Temp%\CustomConfigurationDemo\FileMoveTest.txt" operation="copy" /> <file source="%Temp%\CustomConfigurationDemo\FileMoveTest.txt" target="%Temp%\CustomConfigurationDemo\FileDeleteTest.txt" operation="move" /> <file source="%Temp%\CustomConfigurationDemo\FileDeleteTest.txt" operation="delete" /> |
As you can see, it’s pretty easy to define what to do and again, you can make use of all Environment variables and MDT properties.
Now to something more fancy. We can also create and edit files. Most easy thing is a simple text file.
1 2 3 4 5 6 7 8 9 10 |
<file source="%Temp%\CustomConfigurationDemo\FiletxtTest.txt" operation="update" type="txt" overwrite="true"> <writeline>First Line</writeline> <writeline>Second Line</writeline> </file> <file source="%Temp%\CustomConfigurationDemo\FiletxtTest.txt" operation="update" type="txt" overwrite="false"> <writeline>Third Line</writeline> <writeline>Current User: %Username%</writeline> <writeline>SystemRoot: %SystemRoot%</writeline> </file> |
This will create a new textfile (overwrite=”true” will overwrite any existing file) and write a couple lines of text to it. Then we append a couple more lines with some dynamic content (overwrite=”false” will append the lines if the file exists already).
INI files
OK, that wasn’t really that fancy. How about ini files?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<file source="%Temp%\CustomConfigurationDemo\FileIniTest.ini" operation="create" type="ini"/> <file source="%Temp%\CustomConfigurationDemo\FileIniTest.ini" type="ini" operation="update"> <section name="EnvironmentVariables"> <key name="Username">%Username%</key> <key name="SystemRoot">%SystemRoot%</key> <key name="Temp">#Left("%Temp%",5)#</key> <key name="Architecture"> 64 Bit <rules> <exclude condition="any"> <rule property="ProgramFiles(x86)" operator="empty" /> </exclude> </rules> </key> <key name="Architecture"> 32 Bit <rules> <exclude condition="any"> <rule property="ProgramFiles(x86)" operator="notempty" /> </exclude> </rules> </key> </section> <section name="DeleteSection"> <key name="TestKey1" operation="update">TestValue</key> <key name="TestKey2" operation="update">TestValue2</key> <key name="TestKey1" operation="read" property="MDTDemoPropertyINI1" overwrite="false" /> <key name="TestKey2" operation="read" property="MDTDemoPropertyINI2" overwrite="false" /> </section> </file> <file source="%Temp%\CustomConfigurationDemo\FileIniTest.ini" type="ini" operation="update"> <section name="DeleteSection" operation="delete" /> </file> |
This looks a bit more complicated. So what does it do? Well, first it creates a new ini file. Then it updates this file with two new sections. A section called “EnvironmentVariables” and one called “DeleteSection”. It writes a couple values to both sections. For demonstration purposes I even added a rule when we write the key “Architecture”. So rules can really be applied at any level. In the part that creates the “DeleteSection” you will also see to entries, that read the value of a particular key and store it in a MDT property. The overwrite=”false” simulates the MDT default behaviour, that MT properties will only be set, if there isn’t already a value. But you can always overrule this by setting overwrite=”true”. Finally, the DeleteSection will be deleted again.
So just ask Johan Arwidmark about changing the customsettings.ini at runtime
The code that does the modification of INI files originally comes from Michael Murgolo, a Microsoft consultant, and has been cannibalized a bit to fit into this script. But he has done all the heavy lifting. See the original Blog Post and script file at: Reading and Modifying INI Files with Scripts.
XML Files
Finally, we can also make changes to an XML file. But not only changes, as shown before, we can also read from an XML file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
<file source="%Temp%\CustomConfigurationDemo\FileXMLTest.xml" operation="create" type="xml" overwrite="true"> <DemoRoot></DemoRoot> </file> <file source="%Temp%\CustomConfigurationDemo\FileXMLTest.xml" operation="update" type="xml"> <node operation="create"> <xpath><![CDATA[/DemoRoot]]></xpath> <node> <Demo1>Hello World</Demo1> <Demo2></Demo2> <Demo3></Demo3> </node> </node> <node operation="delete" occurence="all"> <xpath><![CDATA[//Demo1]]></xpath> </node> <node operation="update"> <xpath><![CDATA[//Demo2]]></xpath> <value>110</value> <attribute name="operation" operation="update">Test</attribute> <attribute name="TestAttribute">Test2</attribute> <attribute name="TestAttribute2">Test3</attribute> <attribute name="TestAttribute2" operation="delete" /> </node> <node operation="read"> <xpath><![CDATA[//Demo2]]></xpath> <value property="MDTDemoPropertyXML1" overwrite="false"/> <attribute name="TestAttribute" property="MDTDemoPropertyXML2" overwrite="true" /> </node> <node operation="update"> <xpath><![CDATA[//Demo2]]></xpath> <attribute name="MDTDemoPropertyXML2">#Left("%MDTDemoPropertyXML2%",4)#</attribute> </node> <node operation="delete"> <xpath><![CDATA[//Demo3]]></xpath> </node> <node operation="create"> <xpath><![CDATA[//DemoRoot]]></xpath> <node position="child"> <Demo3> <WithChild> <AndSome attributes="HelloWorld"></AndSome> </WithChild> </Demo3> </node> <node position="firstchild"> <Demo1>Blablabla</Demo1> </node> </node> </file> |
Woah, that’s a lot to read. Let’s go through it step by step. First we create a new xml file with a “DemoRoot” root element.
1 2 3 |
<file source="%Temp%\CustomConfigurationDemo\FileXMLTest.xml" operation="create" type="xml" overwrite="true"> <DemoRoot></DemoRoot> </file> |
The file will now look like this
Next we update this file we just created and want to add a new node to is. The xpath defines where to create the new node. On default it will be created as a new child to the node defined by the xpath. Optionally we can specify “position” attribute, to create it as “firstchild” or “append” or “prepend” it to that node.
1 2 3 4 5 6 7 8 9 |
<file source="%Temp%\CustomConfigurationDemo\FileXMLTest.xml" operation="update" type="xml"> <node operation="create"> <xpath><![CDATA[/DemoRoot]]></xpath> <node> <Demo1>Hello World</Demo1> <Demo2></Demo2> <Demo3></Demo3> </node> </node> |
Now the XML file will look like
OK, now we delete the “Demo1” node and update the “Demo2” node with a new value and some attributes
1 2 3 4 5 6 7 8 9 10 11 |
<node operation="delete" occurence="all"> <xpath><![CDATA[//Demo1]]></xpath> </node> <node operation="update"> <xpath><![CDATA[//Demo2]]></xpath> <value>110</value> <attribute name="operation" operation="update">Test</attribute> <attribute name="TestAttribute">Test2</attribute> <attribute name="TestAttribute2">Test3</attribute> <attribute name="TestAttribute2" operation="delete" /> </node> |
The xml file now looks like
Where is the “TestAttribute2” that we just added? Well, we immediately deleted it again. Does that make sense at all? Not really, but it’s fun
Let’s read a value now and re-use it
1 2 3 4 5 6 7 8 9 |
<node operation="read"> <xpath><![CDATA[//Demo2]]></xpath> <value property="MDTDemoPropertyXML1" overwrite="false"/> <attribute name="TestAttribute" property="MDTDemoPropertyXML2" overwrite="true" /> </node> <node operation="update"> <xpath><![CDATA[//Demo2]]></xpath> <attribute name="MDTDemoPropertyXML2">#Left("%MDTDemoPropertyXML2%",4)#</attribute> </node> |
Here we store the value of the Node (110) in the MDT property “MDTDemoPropertyXML1” (what a stupid name). And the value of the attribute “TestAttribute” (Test2) into the MDT property “MDTDemoPropertyXML2” (not getting better). Finally, we create a new attribute called “MyAttribute” and add the first 4 characters of the MDT Property “MDTDemoPropertyXML2” in it (Test). Sounds confusing? Naaaah, let’s have a look on the xml:
And a couple more steps that create, update, and delete some nodes.
While that’s not really a good real-life example, it demonstrates the possibilities. A real-life sample could be to update the unattend.xml file to support an offline domain join (a detailed blog post about it will be published soon), which currently isn’t something that’s supported by MDT out of the box. So that’s the configuration that could be used for this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!-- Updates the unattend.xml file for an offline domain join--> <file source="{TODO:Path_to_Unattend.xml}\unattend.xml" operation="update" type="xml"> <rules> <include> <rule property="OfflineDomainJoinBlob" operator="notempty" /> </include>> </rules> <node operation="delete"> <xpath><![CDATA[//settings[@pass="specialize"]/component[@name="Microsoft-Windows-UnattendedJoin"]/Identification]]></xpath> </node> <node operation="create"> <xpath><![CDATA[//settings[@pass="specialize"]/component[@name="Microsoft-Windows-UnattendedJoin"]]]></xpath> <node position="child"> <Identification> <Provisioning> <AccountData>%OfflineDomainJoinBlob%</AccountData> </Provisioning> </Identification> </node> </node> </file> |
If the “OfflineDomainJoinBlob” property has a value it will remove all the other domain/workgorup information and add some new nodes to the unattend.xml file, which will allow the computer to be joined to the domain, while not being connected to the network. As said, that’s something I will publish pretty soon.
Integrating in MDT
Normally all you need to execute this script is putting the script and the XML file in the MDT scripts folder (MDT scripts package in ConfigMgr). And then add a “Run Command Line” step to your task sequence, that executes “cscript.exe %ScriptRoot%ZTICustomConfiguration.wsf”. Where you add the step, depends on the actions, that are being executed. If it’s some kind of clean-up script, it’s probably best to put it somewhere at the end. If it updates the unattend.xml, it should probably be somehwere before the “Configure” step, etc. Due to the Rules, you can also call it several times during the Task Sequence. You would then just make sure, that your steps only run at the appropriate phase.
Optionally, you can specify what configuration sets shall be processed. To do this, you need to add a new list property to MDT. You do this by adding the following to your customsettings.ini
1 2 3 4 5 6 |
[Settings] Priority=Init,... Properties=ConfigurationSet(*) [Init] ConfigurationSet001=Demo |
Configuration(*) creates a new MDT list property. And then you simply add all the configuration sets you would like to have processed. If you don’t specify one, the Configuration Set “Default” will be processed if available.
The script will also accept these additional properties:
- CCSkipFolder –> Set to “YES” to skip all folder jobs
- CCSkipFile –> Set to “YES” to skip all file jobs
- CCSkipRegistry –> Set to “YES” to skip all registry jobs
- CCSkipLoadHive –> Set to “YES” to skip loading of additional registry hives
What about a FrontEnd?
Why do I need to fiddle around with an XML file? Why isn’t there a nifty frontend for this?
Good question, as I simply haven’t found the time yet to create one and it would probably look awful. So if there is anyone interested in tackling this task, he/she would get my full support on it. So feel free to contact me or just get used to XMLNotepad, Notepad++ or any other nice text-editor with XML capabilities.
I still consider this script Beta, even if I use it already since quite some time. So please go to CodePlex and download the files. Have a look on the supplied xml file, which is used as some initial documentation as well. Implement and test your changes. And give it a try. If something doesn’t work as expected or you have some good ideas on how to extend this solution or make it a bit more comfortable, just get back to me. The next blog post will be about testing and troubleshooting this script and the xml file.
Recent Comments