There is a part two to this post here.
I’m currently involved with the setting up of a new SCCM Site that spans a number of physical sites. Each site will have at least one Distribution Point (DP). During Operating System Deployment (OSD), we wanted to be able to put the new computer account into a specific OU depending on its location.
One way of doing this I was shown would be a script in the Task Sequence that looks at the Default Gateway of the computer and then sets the OU based on that. Unfortunately this would mean many entries for all our subnets and it would take a lot of maintenance.
Another possibility could be to let the computer account be created in a default OU and then use a script later in the Task Sequence – after the computer becomes domain-joined – to move it to a final OU location based on the AD Site detected.
Instead I found a way to determine the Distribution Point involved in OSD at the start of the Task Sequence and then use that to set the Target OU for the domain join step.
As is usually the case, I decided to tackle this with PowerShell, so first the Boot Image had to have the PowerShell and .NET components added to it and the F8 command prompt feature needed to be enabled too (a checkbox). Once done, I PXE booted a new VM; started the OSD going; pressed F8 to get a CMD window up; and typed “PowerShell” to change the CMD console into a PowerShell console.
My intention was to have a look to see what Task Sequence variables were present at this early stage of OSD to see if any referenced the DP hostname. A quick look online showed that to read and write Task Sequence variables, you need to use the COM Object “Microsoft.SMS.TSEnvironment“. A COM Object is instantiated by the -ComObject parameter on the New-Object cmdlet. Once instantiated, you can of course pipe it to Get-Member to get an idea of what it can do:
Not much to see there. Let’s have a look at the GetVariables() method and see what Type of objects the variables actually are:
As you can see, a whopping 194 variables were present – surely the Distribution Point name has to be in there somewhere. Looking at the first variable returned shows it to be of Type [String]. You can access the name of one by its numerical index and then query the associated Value using the Value() method we saw above:
To get those variables into a form you can easily look through or filter in some way, you can turn them into a collection of Objects with a Name property set to the variable name and a Value property obtained by using the Value() method. This is easily done by creating Hashtables using @{} and then casting them to PSCustomObjects, one for each variable name present in the collection.
$TSEnv = New-Object -ComObject 'Microsoft.SMS.TSEnvironment'
$vars = $TSEnv.GetVariables() | ForEach-Object {
[pscustomobject]@{
Name = $_
Value = $TSEnv.Value($_)
}
}
I entered the script on one line (which requires semi-colons between Hashtable elements) and then selected the first 25 values:
You soon find that a lot of values are empty, or are very short or (as I saw later in the list are very long). With the variables all now a collection of objects, it’s easy to filter them to ones that might be of interest by looking at the length of the variable’s value property:
$vars | Where-Object { ($_.Value.Length -gt 3) -and ($_.Value.Length -lt 30) } | Select-Object -First 25<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>
This gives more interesting values (note the Default Gateway I mentioned earlier). There’s also a variable called _SMSTSMP which is the Management Point:
I did eventually spot a whole group of variables whose value included a URL on the Distribution Point. These variable names all included the ID of a Package in them (and in the value URLs). Here is an example one. The name is in the form _SMSTSHTTPxxxnnnnn where xxxnnnn is the Package ID, of which xxx is the SCCM Site Code:
Armed with this information, it’s now straightforward to slice out the DP’s hostname from one of these values using some .NET string methods and then put it into a new variable to return. In the unlikely event that something should go wrong, I’ll return the value “ERROR”.
param (
[ValidateLength(3,3)]
[string]$SiteCode = 'CC1'
)
$TSEnv = New-Object -ComObject 'Microsoft.SMS.TSEnvironment'
try {
$allVars = $TSEnv.GetVariables() | ForEach-Object {
[pscustomobject]@{
Name = $_
Value = $TSEnv.Value($_)
}
}
$DPvalue = @($allVars | Where-Object { $_.Name -like "_SMSTSHTTP$($SiteCode)*" })[0].Value
$startIndex = $DPValue.IndexOf('://') + 3
$endIndex = $DPValue.IndexOf('/', $startIndex)
$DPName = $DPvalue.Substring($startIndex, $endIndex-$startIndex)
$TSEnv.Value('DPName') = $DPName
} catch {
$TSEnv.Value('DPName') = 'ERROR'
}
I’ve added an optional SiteCode parameter (which is validated as being exactly 3 characters long) and for which I’ve added a default value for my own fictitious environment.
The relevant variable name is found by filtering the collection of constructed variable objects by looking for those whose name matches (i.e. are “-like“) _SMSTSHTTPxxx* where xxx is replaced by the $SiteCode variable by means of the Sub-Expression Operator $(). Note the need for double-quotes to allow this substitution to take place. Since the result of the Where-Object cmdlet may or may not return more than one item, the filter is wrapped inside the array sub-expression @() to guarantee an array is created, even if it is only of one element. That makes it safe to grab the first returned value by indexing into the array with [0]. Finally, it is necessary to get just the actual Value string property using the .Value at the end of the line! If no variable names match, the line will throw an exception as the array will have no elements – the exception will be caught by the catch block.
Chopping out the DP name is simply a case of starting with the first character after the position of “://” in the string and then chopping up to the first “/” after this point. Positions in the string are found using the IndexOf() method overloads on the .NET string class. The first time I use it I give it just the string to look for but the second time I supply a second parameter which is the position at which to start searching (the first character being position 0). Without that, the “/” would match the first one in http:// and not the one delineating the end of the hostname I’m trying to extract. The substring is chopped out using the .NET SubString() method where you tell it which character to start at and how many characters you want.
The script ends by assigning the located value to a Task Sequence variable called “DPName”. If any exception was caught, then the value “ERROR” is returned in the same variable which will later be used to set a default OU location rather than error. Note that the instantiation of the COM Object was not protected by the try{} block. If that fails, I’d really like to know!
The code is then put into a program-less package, distributed to the PXE-capable DPs and then referenced at the top of the OSD Task Sequence as a “Run PowerShell Script” task. (The following screenshots are from a lab setup.) The script is not signed so I needed to tell it to Bypass the Execution Policy:
After this, you insert a “Set Dynamic Variables” task containing a number of “Add Rule – Task Sequence Variable” rules which compare the DPName variable the PowerShell script created with your known DP Fully Qualified Domain Names. Under each match, you use the “Add Variable” button to make it create a new OUName variable with the Distinguished Name of the target OU you wish to use for the location that DP is in. Note the final rule for if the script returned “ERROR” where you choose a default location:
Maintaining this list of DPs is far easier than a massive list of Default Gateways would be. It also doesn’t require anyone to touch the PowerShell code.
The final step is to pass the OUName variable to the Task Sequence step that configures how to join the machine to the domain. This is the “Apply Networking Settings” step. The variable name has to be inside % signs and preceded with the string “LDAP://”:
And that’s it!
Subsequently, I found a much easier way of getting what I wanted in PowerShell. Please see the follow-up post here.