This is the first part in a series of articles about custom Boot Images in SCCM 2007 R2. It will cover the creation of a custom Boot Wizard and making it available for all known and unknown computers. We will be able to choose from a list of available Task Sequences and talk to the right SMS Server of the assigned site. It will be a lot of stuff but just stay with me, I would say it's worth the time spent reading.
In this article we will cover the creation of a custom Boot Wizard. It will show a dynamic list of available Operating System Deployment (OSD) Task Sequences from System Center Configuration Manager (SCCM) 2007. After choosing a Task Sequence it will add the computer to a collection which has this Task Sequence advertised, wait for the Advertisement to be available for the computer and then start this Task Sequence which installs the requested Operating System. This will happen on the SCCM Server the collection resides so we will filter for Task Sequences for the assigned site of the client.
This will enable us to deploy several Operating Systems using a single Boot image. The choice of Boot Images can be configured to your needs and all changes just need to be applied once.
OK, let us just start it. To ease the effort of writing a custom wizard, we simply use the already existing Wizard from the Microsoft Deployment Toolkit (MDT) as our Framework. To do this, we will use the MDT Wizard Editor from Michael Niehaus. This is simple .Net 2.0 application which will assist us with the process of writing the necessary XML files for the Wizard. As an introduction I would like to forward you to the Blog of Todd Hemsell who just wrote a series of articles which deal with the basics of this Wizard Editor.
If you want to follow me through this article, please download the Wizard first. It does not need to be installed. Just save it somewhere and execute it. If you start the wizard, it will not show anything. You will need to open an existing XML File. One of the disadvantages of this wizard is, that it can't create an empty XML file to start with. But this wouldn't be a good guide if there isn't something to solve this. In the download to this Article you will find and a XML File called "MDT_Wizard_Template.xml". Please open this file with the Wizard Editor (you might want to create a copy first and give it a different, more meaningful name). It can be used as a starting point for your own wizards.
One of the first things we want to add to our wizard is a script which contains all the functions we will use. I typically give it the same name I have used for my Wizard XML File just with the extension vbs. So let's create a new Textfile in the folder where you stored your XML File and name it e.g. CustomBootWizard.vbs. In the Boot Wizard chose the "Global" Pane (even if it isn't actually a pane). On the "Settings" Tab click on "Add" choose "Initialization" and click on OK. Under Details you should see now an Entry called "Initialization". Choose this entry and you should see a Textbox in the right area. Just type in the Scriptname of the empty script file you just created (it needs to be in the same directory as the XML File). Now you should be able to open this script for editing. Simply click on the Button "Edit". If it doesn't show an Editor with the file you probably made a typo.
This is the easiest way of adding already existing scripts to your wizard so that you don't need to duplicate any code. You can add as many "Initialization" Statements as you want. There is a second option called "Validation" but this will just do the exact same thing. If you just want to add a couple lines of code or a few functions you can also include this into the XML File itself. This way you don't need to have additional script files. To do this just simply add the "CustomStatement" and type whatever vbscript code you want in the TextArea on the right side.
OK, It's time to add our html code. In the wizard click on the pane "MyPane" (If you changed the name from the template act accordingly) and choose the Tab "HTML". Paste the following code into the white area below:
<H1>Install new Operating System</H1>
<p>The following Operating Systems are available for installation: </p>
<div class=DynamicListBox >
<table id="TSList" datasrc="#tasksequences" mce_src="#tasksequences" width="100%" border=0 cellSpacing=0 language=vbscript onreadystatechange=ReadyInitializeTSList>
<tr valign=top class="DynamicListBoxRow"
onmouseover="javascript:this.className = 'DynamicListBoxRow-over';"
onmouseout="javascript:this.className = 'DynamicListBoxRow';" >
<td class=DynamicListBoxElement width="0px">
<input type=radio name=SelectedItem language=vbscript onPropertyChange="AppItemChange" />
<input type=hidden Name=TSCollectionID disabled datafld="CollectionID"/>
</td>
<td language=vbscript onclick="ClickChildCheckBox" class=DynamicListBoxElement width="100%">
<div>
<Label datafld="Name" class="Larger" ></Label>
</div>
<div>
<Label datafld="Description" dataformatas="HTML"><label class=errmsg style="display: inline;">No Operating Systems are available for this site. Please contact your local IS Support.</label></Label>
</div>
</td>
</tr>
</table>
</div>
<xml id="tasksequences" JavaDSOCompatible=true></xml>
What does this do? Actually I won't go through the whole thing. Todd used a quite similar example on his Blog and explained the table really well. (MDT Wizard Editor: Part 4 - Decipher the HTML) So please read through this first.
We will just concentrate on the stuff he left out. :P
One of the most difficult things will be the so called "XML Data Island" and how it produces a list with several rows. The XML Data Island is used to store XML within an HTML page. This <xml id="tasksequences" JavaDSOCompatible=true></xml> here is actually what we are speaking about. In this case, the xml is empty as we just need it as a placeholder. The content will be filled later by a Webservice call. (Sure you could also write down the necessary XML Elements directly within the HTML page what leads to the question, why not simply creating the Table directly?). But why do we use the JavaDSOCompatible=true attribute in the definition?
Let me explain this on a small example. We assume to have the following XML we would like to bind:
<?xml version="1.0" encoding="utf-8" ?>
<TaskSequences>
<TaskSequence>
<TaskSequenceID>CCC00209</TaskSequenceID>
<CollectionID>CCC000001</CollectionID>
<Name>Windows XP Pro x86</Name>
<Description>Windows XP Professional SP 3 - x86</Description>
</TaskSequence>
<TaskSequence>
<TaskSequenceID>CCC00209</TaskSequenceID>
<CollectionID>CCC002EB</CollectionID>
<Name>Windows XP Pro x64</Name>
<Description>Windows XP Professional SP 3 - x64</Description>
</TaskSequence>
</TaskSequences>
Now we bind this xml to the table. The default behavior (without this JavaDSOCompatible) is as follows: Each Element below the root (TaskSequences) will become a row in our Table. All Elements within this Element will then be the datafields we bind our HTML elements to (e.g. <Label datafld="Name" class="Larger" > ). They become a column so to speak. If any of these Elements does contain additional (Sub)-Elements it will not return a scalar value. Instead it's returned as a rowset. So we would need another table to be able to display the content.
As a sidenote: Each row (rowset) will have an additional "column" called $Text which contains all items in that record, concatenated. It can also be bound to an HTML Element.
However, back to topic. The tricky part is now, that all attributes will be treated as Elements! OK, where is the problem? Let us get back to our first example and imagine it has been returned from a webservice:
<?xml version="1.0" encoding="utf-8" ?>
<TaskSequences xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://maikkoster.com/Deployment">
<TaskSequence>
<TaskSequenceID>CCC00209</TaskSequenceID>
<CollectionID>CCC000001</CollectionID>
<Name>Windows XP Pro x86</Name>
<Description>Windows XP Professional SP 3 - x86</Description>
</TaskSequence>
<TaskSequence>
<TaskSequenceID>CCC00209</TaskSequenceID>
<CollectionID>CCC002EB</CollectionID>
<Name>Windows XP Pro x64</Name>
<Description>Windows XP Professional SP 3 - x64</Description>
</TaskSequence>
</TaskSequences>
You will immediately see the difference. It added some namespace attributes to the root element. If we now bind this to our table it will be interpreted as follows:
<?xml version="1.0" encoding="utf-8" ?>
<TaskSequences>
<xmlns:xsi>http://www.w3.org/2001/XMLSchema-instance</xmlns:xsi>
<xmlns:xsd>http://www.w3.org/2001/XMLSchema</xmlns:xsd>
<xmlns>http://maikkoster.com/Deployment</xmlns>
<TaskSequence>
<TaskSequenceID>CCC00209</TaskSequenceID>
<CollectionID>CCC000001</CollectionID>
<Name>Windows XP Pro x86</Name>
<Description>Windows XP Professional SP 3 - x86</Description>
</TaskSequence>
<TaskSequence>
<TaskSequenceID>CCC00209</TaskSequenceID>
<CollectionID>CCC002EB</CollectionID>
<Name>Windows XP Pro x64</Name>
<Description>Windows XP Professional SP 3 - x64</Description>
</TaskSequence>
</TaskSequences>
And this will create a lot of trouble as our table won't show any entries as nothing has been bound to the new datafields xmlns:xsi, xmlns:xsd or xmlns. So if you are not aware of this behavior you might waste a lot of time trying to find this "error". But how can we get around this?
One Option could be to simply remove the namespaces from the xml. This is not as easy as it sounds and also we might need these namespaces for further processing. And here we come to the Attribute JavaDSOCompatible. If we set this to True, it will change the default behavior how the xml is interpreted. To put it short, it will simply ignore all attributes. And now our Table will show again all our Task Sequences. :-)
The next thing I would like to mention are three functions which are used within our HTML example.
ClickChildCheckBox
This is a Wizard built-in Function which is bound to the onclick event of the Cell containing the Name and Description of our Task Sequence. Actually the user won't see that it is a Cell as it does not have a border, so from his/her perspective, he/she will "choose" an entry from the list. The Function will simply check the first Radiobutton or Checkbox before the clicked Cell. You need to be aware of this if you have several of them within one row.
AppItemChange
This is a custom Function which comes from the original MDT Source files. We will need to include this in our CustomBootWizard.vbs script to be able to use it. The functions consists of a single command:
document.all.item(window.event.srcElement.SourceIndex + 1).Disabled = not window.event.SrcElement.checked
So what does it do?
If the status of a Radiobutton in our Table changes (checked -> unchecked or vice versa) it will go through the Table and simply set the Disabled attribute of the HTML Element after the Radiobutton to the opposite of the checked status.
To much? OK, go back and look at the HTML. We have the Radiobutton. And directly after a hidden and disabled Input Element, which stores our CollectionID. Let's assume we check the second TaskSequence in our List. The AppItemChange Function will now loop through our table. The first Radiobutton is unchecked. So the Input Element will be disabled. The second Radiobutton is checked, so the Input Element will be enabled (Disabled = false). The same works with checkboxes.
That's tricky, but why do we need this? It will help us later to identify the selected values as we only need to take care of enabled items to get the selected value (or a list of selected values in case of checkboxes). I will show you exactly how that works when we come to validate our pane.
ReadyInitializeTSList
Again, this is a custom function so we need to have it available in our CustomBootWizard.vbs script. This function is bound to the onreadystatechange event of the Table. Among others this will be called if the XML file has been parsed and the table has been filled with the values. Have a look at the function itself first:
ButtonNext.Disabled = TRUE
If not TSList.readystate = "complete" then
Exit function
End if
Set oTSList = document.getElementsByName("TSCollectionID")
If oTSList is nothing then
Exit function
End if
If oTSList.Length > 0 then
bFound = FALSE
For each oInput in oTSList
If oInput.Value <> "" and StrComp(oInput.Value, _
ForceAsString(oProperties("CollectionID")),vbTextCompare) = 0 then
document.all.item(oInput.SourceIndex - 1).click
ButtonNext.Disabled = FALSE
bFound = TRUE
End if
Next
If not bFound and oTSList.Item(0).Value <> "" then
' Just select the first item
document.getElementsByName("SelectedItem").Item(0).click
ButtonNext.Disabled = FALSE
End if
End If
What does it do? It will first make sure that all parsing has been completed, and that the table contains at least one row. Then it will loop through all rows and compare the values of the hidden (and disabled :-) ) Input field "TSCollectionID" with the value of a Custom property "CollectionID" (We might have stored this during a prestage process in our MDT Database). If they fit, this row will be selected (it simply emulates a click in the Cell after the hidden Input field and this will then call ClickChildCheckbox as seen before). If none of the values fits to the stored "CollectionID" it will select the first entry. This ensures that at least one entry is selected. For sure you would need to use a slightly different logic if you have a list of Checkboxes.
Finally, before we finish this first, already quite long article, I would like to spent a few more lines on some special behavior of the Wizard regarding some HTML Elements.
Imagine two Textboxes where you want to give the user the possibility to enter some text. This could be the HTML for them:
<table>
<tr>
<td>
<label for=RequiredValue>Required value: </label>
</td>
<td>
<input type=text id="RequiredValue" name="RequiredValue"/>
</td>
</tr>
<tr>
<td>
<label>Optional value: </label>
</td>
<td>
<input type=text id="OptionalValue" name="OptionalValue"/>
</td>
</tr>
</table>
You might be used to use the "for" attribute of a label to specify the associated control. This is part of the Web Content Accessibility Guidelines of the W3C. If you follow these guidelines you can come into trouble as the Wizard uses a different behavior to interpret this. It is using this for some built-in error checking. It will work as follows. Go through the whole document and get all Labels. If a label has a "for" attribute check if the content of the associated Textbox is empty. If it is empty unhide the label and disable the "NEXT" button. In my case I wasn't aware of this and tried to add an optional Textbox to a wizard pane. In most cases there has been some content in the Textbox, so not problem. But at one time it was left empty and it was not possible to go to the next pane. As the NEXT Button stays disabled as long as no text has been entered. Actually this is a great and easy way to implement some simple Error checking. E.G. use something like this to make use of it:
<input type=text id="RequiredValue" name="RequiredValue"/><label class=ErrMsg for=RequiredValue>* Required (MISSING)</label>
and it will show the label if no text has been entered with red text and yellow background :-)
And the last thing for today. Arrays and the built-in SaveAllDataElements Function!
The wizard has a built-in Function which can be called to ease the work of getting the values from the pane and storing them in local variables. Simply call the Built-in Function SaveAllDataElements at the beginning of your validate function and it will go through the whole html of the current pane and save the entered values to a Dictionary object called oProperties. After this Function has been executed you will be able to access a value by simply calling oProperties("MyName"). If there are several controls with the same name like "TSCollectionID" in our example the value of oProperties("TSCollectionID") would be an array. Not a scalar value! This applies even if all but one are disabled (disabled controls does not get saved by default). To get the first item of the array simply call oProperties("TSCollectionID")(0). There is a corresponding PopulateElements function which will be executed automatically from the wizard when a user enters a pane. This function will go through all controls and add the value of any available property with the same name as the control.
The next article will get us through the steps of getting the Task Sequences to choose from, adding the computer to a collection on the correct sccm server and some stuff around this. Finally in the third article we will see a solution to deploy our Custom Boot Wizard out to all Distribution Points and make it bootable via PXE for all Known and Unknown Clients. So stay tuned :-)
All files have been made available on our new codeplex page http://mdtcustomizations.codeplex.com/ (Download example files). It has been created as a repository for MDT Scripts, Front ends, Web services and Utilities for use with ConfigMgr/SCCM.
See also
Create your custom Boot Wizard - Display dynamic data
Create your custom Boot Wizard - Execute the wizard, process the results and create the Boot Image
Create your custom Boot Wizard - make it available for all known and unknown Computers
Using a custom Boot Wizard to boot known and unknown computers in SCCM and choose a Task Sequence to run - Step by Step