Share via


Dynamic Server Selection with MDT 2012 and LTI

It is possible to add dynamic server selection to LTI deployments with minimal editing to your current environment.  This will allow you to assign multiple subnets to specific servers.  It will also attempt to verify that the matched server is online via ping.  If the server does not respond, or if no matching server was found, then it will parse the list of available servers, ping each of them, and dynamically build a drop-down list of active servers via LocationServer.xml.

First, you will need to configure your MDT share add extra files to the boot image.  Create a folder in the root of your MDT share named ExtraFiles.  Then open the Properties of your MDT share and select the Windows PE tab.  For each Platform (x86/x64) go to the "Extra directory to add" space and add the local path to the ExtraFiles folder.  This will ensure that any files placed in this folder path will be added to the boot image build.  The ExtraFiles folder itself will be considered the root of the boot drive and any subfolder paths will remain intact.  Open the ExtraFiles folder and create a subfolder named Deploy.  Open Deploy and create another subfolder named Scripts.  We will save a script here later.

Now we need to create two list files: a list of subnets with their associated site name, and a list of site names with their associated MDT UNC paths.  These list files will be placed into a hidden network share.  This network share will be configured to allow anonymous login with read-only rights so that WinPE can access them on each boot.  This makes the process of updating/adding subnets and servers extremely easy and can be done on the fly without requiring a rebuild of the boot image.  We will only be enabling Anonymous Logon, not the Guest account, and only applying Read permissions.  However, if you're not comfortable with this configuration then you can edit the script below to hard-code your arrays.

You will need to create a new hidden share on a server.  This will be the primary location that all machines will query when booted from the MDT boot image.  The files will be small, but it needs to be in a location that will remain online and accessible (you can create this on a NAS but the configuration will be different as these instructions are for Windows 2008 R2).  Once created, open the Advanced Sharing section of the share and adjust the share permissions.  You want to add the account ANONYMOUS LOGON from the local machine and give it Read rights.  Next, open the Security section of the share and again add ANONYMOUS LOGON from the local machine and assign Read & Execute rights.

Now we need to make two small changes to local GPO to allow access to the share.  Open GPEdit.msc and navigate to Computer Configuration/Windows Settings/Security Settings/Local Policies/Security Options.  Locate "Network access: Let Everyone permissions apply to anonymous users" and set it to Enabled.  Next, locate the "Network access: Shares that can be accessed anonymously" option and add the name of your hidden share.  This is just the share name and not the full path (ex: Share$).

Open the local share path and create two files: VLANs.csv and Servers.csv.  In the VLANs file each line will be formatted as [Subnet],[Site Name] (ex: 4,Delaware).  In this particular configuration we assume that the network is in a Layer 3 configuration with the second octet being associated to particular building/location and the third octet is by traffic type.  The script to read this is configured to match only the second octet of the IP on the machine to be imaged, so that is all that is needed on each line.  You can edit the script to look at the third octet or any combination, but we will get to that later.  Just make sure that what you put in the VLAN file matches what you search for in the script.

Each line of the Servers file should be formatted as [Site Name],[MDT UNC Path] (ex: Delaware,\MDT-Delaware\Prod$).  Be sure that your site names match what you're setting in column two of the VLANs file.  It may seem a bit redundant to have this split into two files when the UNC path could be added as a third column in the VLANs file.  However, if you have multiple subnets pointing to the site name then a single file provides more opportunities to mistype a UNC path.  It also would require some de-duplication of site names and paths in the event that the drop-down menu needs to be built.

Next, save the contents of the following script to ExtraFiles\Deploy\Scripts\MDT_Dynamic_Sites.vbs.

001.'==========================================================================
002.'
003.' VBScript Source File -- Created with SAPIEN Technologies PrimalScript 2009
004.'
005.' NAME: MDT_Dynamic_Sites.vbs
006.'
007.' AUTHOR: Sam McLaughlin
008.' DATE  : 10/29/2014
009.'
010.' COMMENT: Compares local IP to a list of VLANs in VLANs.csv to find a matching
011.'          site name. It then compares the site name to the list of servers
012.'          in Servers.csv to get the share path. If no matching VLAN is found
013.'          or if the specified server is not pingable then it will ping all
014.'          servers in Servers.csv and build a drop-down list of available
015.'          locations.
016.'
017.'==========================================================================
018. 
019.Const ForReading = 1
020. 
021.Dim objShell, objFSO, objWMIService, strVLANFile
022.Dim strServerFile, strOctet, i, x, strLocation, strServerPath
023.Dim arrVLANs(), arrServers()
024. 
025.Set objShell = CreateObject("WScript.Shell")
026.Set objFSO = CreateObject("Scripting.FileSystemObject")
027.Set objWMIService = GetObject("winmgmts://./root/CIMV2")
028. 
029.strVLANFile = "\\Server\Share$\VLANs.csv"   'Location of the VLAN file in CSV format.
030.strServerFile = "\\Server\Share$\Servers.csv"   ' Location of the Server list file in CSV format.
031. 
032.strOctet=Split(FindIP,".")  'Verify the machine has an IP and split it into the 4 octets.
033.BuildArrays()  'Import the two files and dynamically build arrays for their contents.
034. 
035.'Looks at the first column of the VLAN array to find a match to the second octet and
036.'then gets the site name from the second column. Once the site name is found it looks
037.'at the first column of the server array to find a matching site name and then gets
038.'the UNC path from the second column.
039.For i = 0 to UBound(arrVLANs)
040.    If arrVLANs(i,0) = strOctet(1) Then
041.        For x = 0 To UBound(arrServers)
042.            If arrServers(x,0) = arrVLANs(i,1) Then strServerPath = arrServers(x,1)
043.        Next
044.    End If
045.Next
046. 
047.'If a matching site was found then it starts adjusting the bootstrap.ini for the
048.'associated UNC path. If no site was found then it adjusts bootstrap.ini so it
049.'will use a LocationServer.xml file.
050.If strServerPath <> "" Then
051.    AdjustForServer(strServerPath)
052.Else
053.    AdjustForServer("Unknown")
054.End If
055. 
056.Function FindIP()
057.    Dim colItems, objItem, strIPAddress, strIP
058. 
059.    Set colItems = objWMIService.ExecQuery("SELECT * FROM " & _
060.    "Win32_NetworkAdapterConfiguration WHERE IPEnabled=True")
061. 
062.    'Weeds out invalid IP addresses. Also eliminates addresses over 15 characters to
063.    'prevent it from returning an IPV6 address.
064.    For Each  objItem In  colItems
065.        For Each  strIPAddress In  objItem.IPAddress
066.            If strIPAddress = "0.0.0.0" Or  strIPAddress = ""  Or _
067.            strIPAddress = "undefined"  Or Len(strIPAddress) > 15 Then
068.            Else
069.                strIP = strIPAddress
070.            End If
071.        Next
072.    Next
073. 
074.    'If no address or a default MS IP is returned then exit.
075.    If strIP = ""  Or Left(strIP,3) = "169" Then
076.        WScript.Echo "No valid IP address found."  & vbCrLf & "Unable to build site list."
077.        WScript.Quit
078.    End If
079.    FindIP = strIP
080.End Function
081. 
082.Function BuildArrays()
083.    Dim objInFile, strRow, strColumn, iLength, arrEntries
084. 
085.    'Verify that the list files are accessibled and exit if not.
086.    If Not  objFSO.FileExists(strVLANFile) And Not  objFSO.FileExists(strServerFile) Then
087.        WScript.Echo "Cannot locate site/server lists."  & _
088.        vbCrLf & "Unable to build site list."
089.        WScript.Quit
090.    End If
091. 
092.    'Load the VLAN file to check length, adjust array to length, then close file.
093.    Set objInFile = objFSO.OpenTextFile(strVLANFile, ForReading, True)
094.    objInFile.ReadAll
095.    iLength = CInt(objInFile.Line-1) 
096.    ReDim arrVLANs(iLength,1)
097.    objInFile.Close
098. 
099.    'Reopen VLAN file and load contents into array.
100.    Set objInFile = objFSO.OpenTextFile(strVLANFile, ForReading, True)
101.    strRow = 0
102.    Do Until  objInFile.AtEndOfStream 
103.        arrEntries = Split(objInFile.ReadLine,",")
104.        arrVLANs(strRow,0) = arrEntries(0)
105.        arrVLANs(strRow,1) = arrEntries(1)
106.        strRow = strRow + 1
107.    Loop
108.    objInFile.close
109. 
110.    'Load the Server file to check length, adjust array to length, then close file.
111.    Set objInFile = objFSO.OpenTextFile(strServerFile, ForReading, True)
112.    objInFile.ReadAll
113.    iLength = CInt(objInFile.Line-1) 
114.    ReDim arrServers(iLength,1)
115.    objInFile.Close
116. 
117.    'Reopen Server file and load contents into array.
118.    Set objInFile = objFSO.OpenTextFile(strServerFile, ForReading, True)
119.    strRow = 0
120.    Do Until  objInFile.AtEndOfStream 
121.        arrEntries = Split(objInFile.ReadLine,",")
122.        arrServers(strRow,0) = arrEntries(0)
123.        arrServers(strRow,1) = arrEntries(1)
124.        strRow = strRow + 1
125.    Loop
126.    objInFile.close
127.End Function
128. 
129.Function AdjustForServer(strSharePath)
130.    Dim intFound, strServer, objInFile, strText, strOutput, objOutFile
131. 
132.    intFound = 0
133.    strServer = Split(strSharePath,"\")
134. 
135.    'Attempt to ping matching server. If not pingable then set to Unknown.
136.    If Not  PingStatus(strServer(2)) Then strSharePath = "Unknown"
137.     
138.    'Parse contents of bootstrap.ini into a variable in prep for rewrite.
139.    Set objInFile = objFSO.OpenTextFile("X:\Deploy\Scripts\Bootstrap.ini", ForReading, True)
140.    Do Until  objInFile.AtEndOfStream
141.        strText = objInFile.ReadLine
142.        If UCase(Left(strText,10)) = "DEPLOYROOT" Or  _
143.        UCase(Left(strText,11)) = ";DEPLOYROOT"  Then
144.            intFound = 1
145.            'If server is Unknown then eliminate DeployRoot line.
146.            'Otherwise, rewrite line to point to new server.
147.            If strSharePath <> "Unknown" Then
148.                strOutput = strOutput & "DeployRoot="  & strSharePath & vbCrLf
149.            End If
150.        Else
151.            strOutput = strOutput & strText & vbCrLf
152.        End If
153.    Loop
154.     
155.    'If server is valid but no DeployRoot line was found then add it to contents.
156.    If intFound = 0 And strSharePath <> "Unknown" Then  strOutput = strOutput & _
157.    "DeployRoot=" & strSharePath
158.    objInFile.Close
159. 
160.    'Rewrite bootstrap.ini on cache drive.
161.    Set objOutFile = objFSO.CreateTextFile("X:\Deploy\Scripts\Bootstrap.ini")
162.    objOutFile.Write strOutput
163.    objOutFile.Close
164. 
165.    'Delete LocationServer.xml from cache drive if it exists.
166.    If objFSO.FileExists("X:\Deploy\Control\LocationServer.xml") Then  _
167.    objFSO.DeleteFile("X:\Deploy\Control\LocationServer.xml")
168.     
169.    'If server is Unknown then build new LocationServer.xml file.
170.    If strSharePath = "Unknown" Then  BuildLocationXML()
171.End Function
172. 
173.Function PingStatus(strComputer) 
174.    Dim colPings, objStatus
175. 
176.    Set colPings = objWMIService.ExecQuery("Select * From Win32_PingStatus " & _
177.    "Where Address = '" & strComputer & "'")  
178.    For Each  objStatus In  colPings  
179.        If IsNull(objStatus.StatusCode) Or objStatus.StatusCode <> 0 Then  
180.            PingStatus = False
181.        Else
182.            PingStatus = True
183.        End If
184.    Next
185.End Function
186. 
187.Function BuildLocationXML()
188.    Dim strOutput, s, n, strServer, strOutFile
189. 
190.    'Create the Control folder on the cache drive if it doesn't exist.
191.    If Not  objFSO.FolderExists("X:\Deploy\Control") Then  _
192.    objFSO.CreateFolder("X:\Deploy\Control")
193.     
194.    'Load header lines into a variable
195.    strOutput = "<?xml version=""1.0"" encoding=""utf-8"" ?>" & vbCrLf &_
196.                "<servers>" & vbCrLf &_
197.                "    <QueryDefault></QueryDefault>" & vbCrLf
198.    s = 1
199.     
200.    'Ping each server in the array and build list of servers that are online.
201.    For n = 0 To UBound(arrServers)
202.        strServer = Split(arrServers(n,1),"\")
203.        If PingStatus(strServer(2)) Then
204.            strOutput = strOutput & "    <server>"  & vbCrLf &_
205.            "        <serverid>" & s & "</serverid>" & vbCrLf &_
206.            "        <friendlyname>" & vbCrLf &_
207.            "          " & arrServers(n,0) & vbCrLf &_
208.            "        </friendlyname>" & vbCrLf &_
209.            "        <UNCPath>" & arrServers(n,1) & "</UNCPath>" & vbCrLf &_
210.            "    </server>" & vbCrLf
211.            s = s + 1
212.        End If
213.    Next
214.    strOutput = strOutput & "</servers>"
215. 
216.    'Write the LocationServer.xml file.
217.    Set strOutFile = objFSO.CreateTextFile("X:\Deploy\Control\LocationServer.xml")
218.    strOutFile.Write(strOutput)
219.    strOutFile.Close
220.End Function

What does this script do?  Well there are comments throughout the script but here's a quick breakdown:

  • Validates the machine has an IP and then splits it into the four octets.
    • If not, throws an error and exits script.
  • Checks to make sure it can access both list files.
    • If not, throws an error and exits script.
  • Parses both file lists, dynamically builds arrays for each, then loads the file contents into the arrays
  • Compares the second octet to the list of VLANs to find a match and then pulls the site name.
  • Compares the site name to the list of servers to find a match and pulls the UNC path.
  • If no server is found then sets the variable to "Unknown".
  • Attempts to ping any specified server and if there's no response it sets the server to "Unknown".
  • Loads the contents of the existing Bootstrap.ini file into a variable and looks for the DeployRoot line.
  • Replaces the DeployRoot line with the new UNC path.
    • If the server was valid and DeployRoot doesn't exist then adds it as the last line.
    • If server is "Unknown" then removes DeployRoot in preparation for the use of LocationServer.xml.
  • Rewrites the Bootstrap.ini file.
  • Deletes any existing LocationServer.xml file.
  • If the server was valid then script ends and LTI launches.
  • If the server was "Unknown" then it pings all servers in the Servers array and builds a LocationServer.xml file with all servers that respond.

Lines 28 and 29 contain variables with the paths to the two files, so you will need to change them to your appropriate share path.

Lines 38-44 contain the code for looking at the second octet.  You can change to the third octet by changing strOctet(1) to strOctet(2) or edit for combinations.  You can also edit it to account for a slightly less linear network.  Let's say most of your network is Layer 3 and fits the second octet scenario but you have multiple sites that fall under the same second octet and are assigned by third octet (ex: 10.5.8.0 is Lakeside, 10.5.9.0 is Mountain View, etc.).  You can edit the code from this:

For i = 0 to UBound(arrVLANs)
    If arrVLANs(i,0) = strOctet(1) Then
        For x = 0 To UBound(arrServers)
            If arrServers(x,0) = arrVLANs(i,1) Then strServerPath = arrServers(x,1)
        Next
    End If
Next

To look like this:

If strOctet(1) = "5" Then
    For i = 0 to UBound(arrVLANs)
        If arrVLANs(i,0) = strOctet(1) & "." & strOctet(2) Then
            For x = 0 To UBound(arrServers)
                If arrServers(x,0) = arrVLANs(i,1) Then strServerPath = arrServers(x,1)
            Next
        End If
    Next
Else
    For i = 0 to UBound(arrVLANs)
        If arrVLANs(i,0) = strOctet(1) Then
            For x = 0 To UBound(arrServers)
                If arrServers(x,0) = arrVLANs(i,1) Then strServerPath = arrServers(x,1)
            Next
        End If
    Next
End If

This way if the second octet is 5 then it only searches for 5.x segments.  Inside your VLANs file you would just need to add the third octet to the first column for those required (ex: "4,Delaware", "5.8,Lakeside", and "5.9,Mountain View").

Now we just need to tell MDT to run the script.  Open up Explorer to C:\Program Files\Microsoft Deployment Toolkit\Templates.  There are two XML files that will need to be edited: Unattend_PE_x64.xml and Unattend_PE_x86.xml.  When you open the files you should see a RunSynchronous section as follows:

<RunSynchronous>
    <RunSynchronousCommand wcm:action="add">
        <Description>Lite Touch PE</Description>
        <Order>1</Order>
        <Path>wscript.exe X:\Deploy\Scripts\LiteTouch.wsf</Path>
    </RunSynchronousCommand>
</RunSynchronous>

You will need to duplicate the RunSynchronousCommand section so that the new script runs prior to launching Lite Touch.  It should look as follows:

<RunSynchronous>
    <RunSynchronousCommand wcm:action="add">
        <Description>MDT Dynamic Sites</Description>
        <Order>1</Order>
        <Path>wscript.exe X:\Deploy\Scripts\MDT_Dynamic_Sites.vbs</Path>
    </RunSynchronousCommand>
    <RunSynchronousCommand wcm:action="add">
        <Description>Lite Touch PE</Description>
        <Order>2</Order>
        <Path>wscript.exe X:\Deploy\Scripts\LiteTouch.wsf</Path>
    </RunSynchronousCommand>
</RunSynchronous>

Make sure that you adjust the Order numbers so that the new script is 1 and LTI is 2.  That's it.  All you need to do is rebuild your boot image and test.

During testing I find it much easier to use virtual machines.  Load only the necessary VM drivers into a separate folder, create a new Selection Profile for those drivers, and in Properties change your boot image to only use that Selection Profile.  This is under the same section where you added the ExtraFiles path but it's under the Drivers and Patches subtab.  This means that while testing it should only take about 5 minutes to rebuild your boot image.