Tag Archives: Microsoft Deployment Toolkit

Migrating to the SCCM UDI for OSD, Part 2c: Driver Handling (continued)

Continued from part 2b:
http://blog.uvm.edu/jgm/2015/03/09/sccm-udi-2b/

In order for this code to function, you will need the UVM-ConfigurationManager.psm1 powershell module. This module is just a function library written in plain PowerShell. I thought it might be useful to split these functions out for future re-use. This method for creating PowerShell modules must be new, as I am sure it did not exist the last time I needed a PowerShell function library. It is a nice addition in that it adds some Class-like functionality to the language that previously was missing.

# UVM Configuraiton Manager Module
# Functions use "SMS" prefix to avoid collision with Microsoft Configuration Manager cmdlets (which use "CM" prefix).

#History:
#  2015-02-19 - Created, added SMSProviderNamespace and SMSObject, SMSFullObject, and SMSClass cmdlets.
#  2015-03-12 - Added SMSSiteNamespace and Add/Get/Remove/Test SMSTSStep cmdlets.  Bug fixes.
Set-PSDebug -Strict

function Add-SMSTSStep {
    param (
        [Parameter(Mandatory=$true)][System.Management.ManagementBaseObject]$TSStep,
        [Parameter(Mandatory=$true)][System.Management.ManagementBaseObject]$TSObject,
        [int32]$StepIndex = 2147483647
    )
    # Adds the Task Sequence Step (which can be wither an Action or Group) specified in 
    #   $TSNewStep to the Task Sequencw or Task Sequence Group supplied in $TSObject. If 
    #   specified, the step will be added at the index localtion specified in $TSStepIndex.
    #   Otherwise, the step will be appended the end of the sequence or group.

    [System.Management.ManagementBaseObject[]]$newSteps = @()
    #Kludge: The max TS step index could not get this hight without crashing the TS:
    if ($StepIndex -eq 2147483647) { 
        $TSObject.steps += $TSStep
    } else {
        [int32]$i = 0
        foreach ($step in $TSObject.steps) {
            if ($i -eq $StepIndex) {
                $newSteps += $TSStep
            }
            $newSteps += $step
            $i ++
        }
        $TSObject.steps = $newSteps
    }
    return $TSObject
    $TSObject.Dispose()
}

function Get-SMSSiteNamespace {
    #Returns the namespace for the SMS Site on the local server.  
    #  Useful for Get-WMIObject commands.
    $SMSPN = Get-SMSProviderNamespace
    [string]$namespace = $SMSPN.Substring(($SMSPN.IndexOf('\root') + 1))
    return $namespace
}

function Get-SMSProviderNamespace {
    # Returns the SMS_ProviderLocation NamespacePath string on the local server.  
    #   Useful when calling SMS objects by their full path using the [wmi]$smsObjectPath constructor.
    $SMSPL = Get-WmiObject -Query "select * from sms_providerlocation" -Namespace root/sms
    return $SMSPL.NamespacePath
    $SMSPL.Dispose()
}

function Get-SMSSiteCode {
    $SMSPL = Get-WmiObject -Query "select * from sms_providerlocation" -Namespace root/sms
    return $SMSPL.SiteCode
    $SMSPL.Dispose()
}

function Get-SMSFullObject {
    param (
        [string]$namespace = (Get-SMSSiteNamespace),
        [Parameter(Mandatory=$true)][string]$class,
        [Parameter(Mandatory=$true)][string]$filter
    )
    #Do a WMI query to retrieve a specific WMI object.  The "filter" must be constructed to return only one result:
    $looseObject = Get-WmiObject -namespace $namespace -class $class -filter $filter
    #Directly retrieve the object that was queried for above.  The resultanat object will have all properties available.
    return [wmi] $looseObject.__Path
    $looseObject.Dispose()
}

function Get-SMSTSStepIndex {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)][System.Management.ManagementBaseObject]$TSObject,
        [Parameter(Mandatory=$true)][string]$TSStepName
    )
    [int]$index = 0
    [bool]$found = $false
    foreach ($step in $TSObject.steps) {
        if (($step.Name -eq $TSStepName) -and (($step.__DYNASTY -eq "SMS_TaskSequence_Step") -or ($step.__DYNASTY -eq "SMS_TaskSequence"))) {
            $found = $true
            break
        }
        $index ++
    }
    if ($found) {
        return $index
    } else {
        throw [string]$("Step with name '$TSStepName' was not found in the specified TaskSequence object '" + $TSObject.name + "'.")
    }
    $TSObject.Dispose()
}

function New-SMSClass {
    param (
        [Parameter(Mandatory=$false)][wmi]$smsObject,
        [Parameter(Mandatory=$false)][string]$smsClass
    )
    if ($smsClass) {
        [string] $classPath = (Get-SMSProviderNameSpace) + ':' + $smsClass
    } elseif ($smsObject) {
        [string] $classPath = $smsObject.__NAMESPACE + ':' + $smsObject.__CLASS
    } else {
        Write-Host "Get-SMSClass requires either an SMS Object (-smsObject) or an SMS Class Name (-class) as input"
    }
    if ($classpath) {
        try {
            $outClass = [wmiclass] $classPath
        } catch [System.Management.Automation.RuntimeException] {
            Write-Host "An error occurred."
            Write-Host ""
            Write-Host "Perhaps you did not provide a valid class name? Try running the following command for a full list of valid classes: "
            Write-Host $([string]'Get-WmiObject -list -Namespace $namespace | select -property name | Sort-Object -Property name | ? -Property name -match ' + "'" + 'SMS_|BDD_' + "'")
        }
        return $outClass
        $outClass.Dispose()
    }
}

function New-SMSObject {
    param (
        [string]$namespace = (Get-SMSProviderNamespace),
        [Parameter(Mandatory=$true)][string]$class
    )
    [string] $wmiPath = $namespace + ':' + $class
    $wmiClass = [wmiclass] $wmiPath
    return $wmiClass.CreateInstance()
}

function Remove-SMSTSStep {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)][System.Management.ManagementBaseObject]$TSObject,
        [Parameter(Mandatory=$true)][string]$TSStepName
    )
    # Removes the first instance of a Task Sequence Step with the .name attribute matchine the import param $TSStepName.
    #   Intent was to return a new object with the step removed, but the script modifies the input object as-is owing to some incomprehensible 
    #   linkage between the function object $TSObject and the supplied input object.  Seems like a scope violation, but I guess that's just WMI.
    # Requires:
    #   $TSObject - An ManagementBaseObject that must be of the SMS_TaskSequence or SMS_TaskSequence_Group class.  Must contain a "steps" property.
    #   $TSStepName - Must be the full name of the TSStep or TSGroup to be removed from the TSSteps object.
    # Returns:
    #   A new TSObject with the first instance of the specified step removed.
    [bool]$found = $false
    foreach ($step in $TSObject.steps) {
        if (($step.Name -eq $TSStepName) -and (($step.__DYNASTY -eq "SMS_TaskSequence_Step") -or ($step.__DYNASTY -eq "SMS_TaskSequence"))) {
            $killStep = $step
            $found = $true
            break
        }
    }
    if ($found) {
        #-- This will not work because the Array is of fixed length.  The remove method is present but does not actually work:
        #$groupSteps.Remove($killStep)
        #-- This does work, but I can't really see why.  It is nice compact code, but I cannot bring myself to trust it: 
        #$newSteps = $postInstTSGroup.steps -ne $killStep

        #-- The following works... note that we have to cast $newSteps as an array of managementBaseObjects, otherwise we
        #   will get casting error that 'PSObject' cannot be cast to type 'ManagementBaseObject'.  
        [System.Management.ManagementBaseObject[]]$newSteps = $TSObject.steps | ? {$_.Name -ne $killStep.Name}
        #Use local scope in case there is a global $newSteps.  PowerShell should prefer local, but I like to play it safe.
        $TSObject.steps = $local:newSteps
        return $TSObject
        $killStep.Dispose()
    } else {
        throw "Specified step was not found within the task sequence object."
    }
    $TSObject.Dispose()
}

function Test-SMSTSStep {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)][System.Management.ManagementBaseObject]$TSObject,
        [Parameter(Mandatory=$true)][string]$TSStepName
    )
    # Tests the input $TSObject (which needs to be of Class SMS_TaskSequence or SMS_TaskSequenceGroup) for a 
    #    step with a .name attribute matching the string parameter $TSStepName.
    # Returns: A Boolean $true or $false.
    [bool] $found = $false
    foreach ($step in $TSObject.steps) {
        if (($step.Name -eq $TSStepName) -and (($step.__DYNASTY -eq "SMS_TaskSequence_Step") -or ($step.__DYNASTY -eq "SMS_TaskSequence"))) {
            $found = $true
            break
        }
    }
    return $found
}

Export-ModuleMember -Function Add-SMSTSStep, Get-SMSProviderNameSpace, Get-SMSSiteNamespace, Get-SMSSiteCode, Get-SMSFullObject, Get-SMSTSStepIndex, New-SMSClass, New-SMSObject, Remove-SMSTSStep, Test-SMSTSStep

In part “2D” of this post, we will explore the client-side script used in this solution…

Next: Drivers – Client-side driver package selection
http://blog.uvm.edu/jgm/2015/03/09/sccm-udi-2d-drivers/


Series Index:

Migrating to the SCCM UDI for OSD, Part 2b: Driver Handling (continued)

Continued from part 2a:
http://blog.uvm.edu/jgm/2015/03/09/sccm-udi-2a/

So, after importing the drivers, we needed an automated method of updating our task sequences so that the drivers can be injected. Words cannot describe the frustration that I felt while dealing with WMI, PowerShell, and the so-called SCCM SDK reference on Task Sequences:
https://msdn.microsoft.com/en-us/library/jj217977.aspx
Let’s just leave it at “it was painful”.

A few days of heads-down programming yielded code that could create a task sequence that contained the necessary driver injection task sequence steps. Another day and a half of coding later, I now am able to update an existing task sequence to contain the driver injection step. The code must be run directly on your SCCM management point:

# Update-DriverInjectionTaskSequence:
# Created 2015-02-19, by J. Greg Mackinnon
# Updated 2015-03-12 - Added ability to update an existing, full OS installation Task Sequence.
#                    - Also removes pre-existing "AutoApplyDrivers" step.  

# Script will update the SCCM Task Sequence named in the mandatory $name parameter. It will add conditional 
# driver package application steps.  One step will be generated for each supported OS/model combination.
# If a driver cannot be found for a "higher level" OS (i.e. Windows 8), the script will attempt to locate
# a "lower level" OS driver package for that model instead (i.e. Windows 7).
# Additionally, supported "peripheral" drivers will be installed for all systems.
# If no matching driver package is detected, an "AutoApplyDrivers" task sequence step will be executed.

# WMI Classes associated with CM Task Sequences (that are relevant to us):
#   SMS_TaskSequencePackage                        <-- The master Task Sequence object
#   SMS_TaskSequence                               <-- Each Task Sequence Package has one of these.
#   SMS_TaskSequence_Group                         <-- Logical groups of actions in the sequence.
#   SMS_TaskSequence_Condition                     <-- A condition that can be attached to an action or group
#      SMS_TaskSequence_WMIConditionExpression        <-- Use "Model" MDT TS Variable instead!
#      SMS_TaskSequence_MakeModelConditionExpression  <-- Does not exist in the GUI! Do not use!
#      SMS_TaskSequence_VariableConditionExpression   <-- Condition based on a TS variable.
#   SMS_TaskSequence_ApplyDriverPackageAction
#   SMS_TaskSequence_AutoApplyAction               <-- Runs an "Auto Apply Drivers" action.

# See them all by running:
#    Get-WmiObject -list -Namespace $namespace | select -property name | ? -Property name -Match "SMS_TaskSequence"

# Helpful resources:
#    The authority... MSDN on programming task sequences (VBScript and C#):
#      https://msdn.microsoft.com/en-us/library/jj217977.aspx
#    Describes how to expand properties from SMS objects with "lazy" properties:
#      http://trevorsullivan.net/2010/09/28/powershell-configmgr-wmi-provider-feat-lazy-properties/
#    Describes using the [wmi] type accelerator to retrieve WMI objects by absolute path:
#      http://windowsitpro.com/scripting/type-accelerators-useful-undocumented-feature-powershell-10
#    Describes the difference between [wmi] and [wmiclass] objects:
#      http://tfl09.blogspot.com/2008/12/powershells-wmiclass-type-accelerator.html
[CmdletBinding()]
Param(
    [Parameter(Mandatory=$True,Position=1)][string]$name,
    [Parameter(Position=2)][string]$namespace
)
Set-PSDebug -Strict

#CM Server info:
#[string]$namespace = 'root\sms\site_' + $SiteCode

#Set the WQL-formatted filter which will return a specific WMI object (Task Sequence Package object):
[string]$TSPackageName = $name

#Specify the name of the Driver Injection Group to be added to the Task Sequence:
[string]$TSGroupName = 'UVM Driver Package Injection Group'

#Supported OS Versions:
[string[]]$OSList = @('Win7','Win8','Win10')

Import-Module 'c:\local\scripts\UVM-ConfigurationManager.psm1'

#Set default value for $namespace, if not provided as a parameter:
if ((-not (Test-Path Variable:\namespace)) -or !$namespace) {[string]$namespace = Get-SMSSiteNamespace}

# Get the Name and PackageID for all driver packages currently defined in SCCM, and put them into an array:
[array]$DPackages = @()
$DPackages = Get-WmiObject -Namespace $namespace -Query "Select Name,PackageID from SMS_DriverPackage" | Select-Object -Property Name,PackageID | Sort-Object -Property Name 

######################################################
########## Begin Create New Driver TS Group ##########
#Create a new Task Sequence Group:
$NewTSGroup = New-SMSObject -class SMS_TaskSequence_Group
$NewTSGroup.Name = $TSGroupName
$NewTSGroup.Description = "Copy this group into a task sequence to replace all pre-existing driver actions."
#Add an action to run the Package Detection script:
$PkgDetectTSAction = New-SMSObject -class SMS_TaskSequence_RunCommandLineAction
$PkgDetectTSAction.Name = "Run the Driver Package Detection Script"
$PkgDetectTSAction.Description = "Run a script to determine which (if any) driver package to apply to the operating " `
    + "system.  This script will set the 'UVMDriverPackageDetected' and 'UVMDriverPackage' variables."
$PkgDetectTSAction.CommandLine = 'cscript.exe %DeployRoot%\Scripts\ZUVMDetectDriverPackage.wsf'
#Create the "Apply Packages" Group":
$ApplyPkgsTSGroup = New-SMSObject -class SMS_TaskSequence_Group
$ApplyPkgsTSGroup.Name = "Apply Driver Packages Group"
$ApplyPkgsTSGroup.Description = "Apply the detected package group and other mandatory groups, if a package was detected."
$TSCondition = New-SMSObject -namespace $namespace -class SMS_TaskSequence_Condition
#Create a Task Sequence Condition Expression object:
$TSConditionExp = New-SMSObject -namespace $namespace -class SMS_TaskSequence_VariableConditionExpression
$TSConditionExp.Operator = "equals"
$TSConditionExp.Value = "YES"
$TSConditionExp.Variable = "UVMDriverPackageDetected"
#Add the condition expression to the "operands" attribute of the condition object:
$TSCondition.Operands = @($TSConditionExp) #  Multiple conditions are possile, use an array.
#Add the Condition object to the condition attribute of the Group object:
$ApplyPkgsTSGroup.Condition = $TSCondition #  Only one condition, not an array.
$TSConditionExp.Dispose()
$TSCondition.Dispose()
foreach ($package in $DPackages) {
    if ($package.name -notmatch '^Other|^WinPE') { # Exclude Other and WinPE packages... these are for "AutoApply" logic only.
        #Create a new Task Sequence Action object:
        $TSAction = New-SMSObject -namespace $namespace -class SMS_TaskSequence_ApplyDriverPackageAction
        #Set the required properties
        $TSAction.name = 'Apply the ' + $package.Name + ' Driver Packge'
        $TSAction.Description = "Conditionally install this driver package, if it matches the UVMDriverPackage TS Environment Variable."
        $TSAction.DriverPackageID = $package.PackageID
        $TSAction.UnsignedDriver = $true

        if ($package.name -notmatch '^Peripherals'){ # Exclude conditional logic for peripherals, because we want all systems to get these. 
            #Create a Task Sequence Condition object:
            $TSCondition = New-SMSObject -namespace $namespace -class SMS_TaskSequence_Condition
            #Create a Task Sequence Condition Expression object:
            $TSConditionExp = New-SMSObject -namespace $namespace -class SMS_TaskSequence_VariableConditionExpression
            $TSConditionExp.Operator = "equals"
            $TSConditionExp.Value = $package.Name
            $TSConditionExp.Variable = "UVMDriverPackage"
            #Add the condition expression to the "operands" attribute of the condition object:
            $TSCondition.Operands = @($TSConditionExp) #  Multiple conditions are possible, use an array.
            #Add the Condition object to the condition attribute of the Action object:
            $TSAction.Condition = $TSCondition #  Only one condition, not an array.
            $TSConditionExp.Dispose()
            $TSCondition.Dispose()
        }
        #Add the TS Action to the parent TS Group:
        $ApplyPkgsTSGroup.Steps += @($TSAction)
        $TSAction.Dispose()
    }
}
#Create the "Auto Apply Drivers" Step":
$AutoApplyTSAction = New-SMSObject -class SMS_TaskSequence_AutoApplyAction
$AutoApplyTSAction.Name = "Auto Apply Drivers Action"
$AutoApplyTSAction.Description = "Automatically apply all matching drivers, ONLY IF a matching driver package was not detected."
$AutoApplyTSAction.UnsignedDriver = $true
$TSCondition = New-SMSObject -namespace $namespace -class SMS_TaskSequence_Condition
#Create a Task Sequence Condition Expression object:
$TSConditionExp = New-SMSObject -namespace $namespace -class SMS_TaskSequence_VariableConditionExpression
$TSConditionExp.Operator = "equals"
$TSConditionExp.Value = "NO"
$TSConditionExp.Variable = "UVMDriverPackageDetected"
#Add the condition expression to the "operands" attribute of the condition object:
$TSCondition.Operands = @($TSConditionExp) #  Multiple conditions are possible, use an array.
#Add the Condition object to the condition attribute of the Group object:
$AutoApplyTSAction.Condition = $TSCondition # Only one condition, not an array.
$TSConditionExp.Dispose()
$TSCondition.Dispose()
#Put the Apply Packages Group into the Root Group:
$NewTSGroup.Steps = @($PkgDetectTSAction,$ApplyPkgsTSGroup,$AutoApplyTSAction)
$PkgDetectTSAction.Dispose()
$ApplyPkgsTSGroup.Dispose()
$AutoApplyTSAction.Dispose()
########### End Create New Driver TS Group ###########
######################################################


######################################################
############# Begin Retrieve Existing TS #############
#Run the WQL queries required to get the fully-populated Task Sequence Package object (no loosely bound parameters)
[string]$filter = "name = '" + $TSPackageName + "'"
$TSP = Get-SMSFullObject -namespace $namespace -class SMS_TaskSequencePackage -filter $filter
#Get a class object for the object retrieved above.  This will allow access to static properties and methods not available in individual WMI objects.
$TSPClass = New-SMSClass -smsObject $TSP 
#For Task Sequence Packages, the GetSequence method allows us to get the sequence associated with a package.
#(Under CM 2012, each Package has one (and only one) Task Sequence)
#  Q: Why do this? The WMI object retrieved above already has a property "sequence", which contains all groups/steps in XML format.  Why can't we use that?
#  A: Because this is XML data that will be challenging to manipulate!  CM has separate classes for more controlled TS step manipulation.
#Note1: Interestingly, the retrieved object as a property "TaskSequence", which is the actual Task Sequence.  WHY!?!?!
#Note2: We also could use "New-SMSObject" to create an entirely new Task Sequence.
$TS = $TSPClass.GetSequence($TSP).TaskSequence
############## End Retrieve Existing TS ##############
######################################################

#Locate the Task Sequence items that need to be modified:
[int32]$exeIndex = Get-SMSTSStepIndex -TSObject $TS -TSStepName 'Execute Task Sequence'
$ExeTSGroup = $TS.steps[$exeIndex]
[int32]$postIndex = Get-SMSTSStepIndex -TSObject $ExeTSGroup -TSStepName 'PostInstall'
$postInstTSGroup = $ExeTSGroup.steps[$postIndex]

#Remove the existing "Auto Apply Drivers" step, if it exists:
[string]$autoApplyStepName = 'Auto Apply Drivers'
if (Test-SMSTSStep -TSObject $postInstTSGroup -TSStepName $autoApplyStepName) {
    Remove-SMSTSStep -TSObject $postInstTSGroup -TSStepName $autoApplyStepName
}   

#Remove the existing UVM Driver Group (if it exists):
if (Test-SMSTSStep -TSObject $postInstTSGroup -TSStepName $TSGroupName) {
    Remove-SMSTSStep -TSObject $postInstTSGroup -TSStepName $TSGroupName
}

#Identify the position within the task sequence group where we will add our new UVM Driver Group:
[int]$i = [int]$(Get-SMSTSStepIndex -TSObject $postInstTSGroup -TSStepName 'Configure') + 1

#Add the new TS Driver Group to the PostInstall Group after the position discovered in the previous step:
Add-SMSTSStep -TSObject $postInstTSGroup -TSStep $NewTSGroup -StepIndex $i

#Walk back up the task sequence tree, updating each parent group with the revised child groups:
Remove-SMSTSStep -TSObject $ExeTSGroup -TSStepName $postInstTSGroup.Name
Add-SMSTSStep -TSObject $ExeTSGroup -TSStep $postInstTSGroup -StepIndex $postIndex
Remove-SMSTSStep -TSObject $TS -TSStepName $ExeTSGroup.Name 
Add-SMSTSStep -TSObject $TS -TSStep $ExeTSGroup -StepIndex $exeIndex
$NewTSGroup.Dispose()
$postInstTSGroup.Dispose()
$ExeTSGroup.Dispose()

#The moment of truth...
#Use the Task Sequence Package class "SetSequence" method to add our new or updated sequence to the task sequence package object:
try {
    $TSPClass.SetSequence($TSP,$TS)
} catch  [System.Management.Automation.MethodInvocationException] {
    [string] $out = "Could not commit the Task Sequence to the Task Sequence Package. "
    $out += "Perhaps this Package is open for editing elsewhere? "
    $out += "Check the Management Point SMSProv.log for details. "
    Write-Error $out
}

#At this point, our Task Sequence has been updated, and the new steps will be available to clients!

# Dispose of all remaining objects: (Do we really need to do this?)
$TS.Dispose()
$TSP.Dispose()
$TSPClass.Dispose()

In order for this code to function, you will need the UVM-ConfigurationManager.psm1 powershell module. This module is just a function library written in plain PowerShell. The contents of this module will be shown in a continuation of this post…

Next: Drivers – Powershell Support Module
http://blog.uvm.edu/jgm/2015/03/09/sccm-udi-2c-drivers/


Series Index:

Migrating to the SCCM UDI for OSD, Part 2a: Driver Handling

Continued from part 1:
http://blog.uvm.edu/jgm/2015/03/09/sccm-udi-1/

Frequent readers of this blog (Anyone? Anyone? Bueller?) will recall my epic series on driver handling under MDT/LiteTouch. For everyone else, you will have to trust me that we came up with an effective and sustainable set of scripts for managing drivers under LiteTouch.

In transitioning to UDI, I hit some pretty serious roadblocks. Notably, UDI does not implement a usable environment variable that can be used to specify the SCCM driver group or package that you want to inject into or make available to your operating system during deployment. Under LTI, we added a stock “Inject Drivers” task sequence step, and used the task sequence variables “DriverGroup001” to specify the path on the deployment share that contained the drivers for the current model of computer.

Under SCCM/UDI, there is no such task sequence variable. Oh sure, the documentation does make reference to a variable “OSDAutoApplyDriverCategoryList” which is used in the “Auto Apply Drivers” task sequence step. But if you set this variable within your running task sequence, it gets ignored by the task sequence step at execution time. While you may find many blogs that provide information to the contrary (by Ben Hunter, no less), my experimentation suggests otherwise. I can see “OSDAutoApplyDriverCategoryList” get set by my script (it shows up in the logs), and then the settings get completely ignored when the driver injection takes place. An MS consultant confirmed this finding, stating that using OSDAutoApplyDriverCategoryList to control driver injection is “impossible”.

“What about using the ‘Apply Driver Package’ task sequence step instead?”, do I hear you ask? Well, that sounds like a good idea, except that ‘Apply Driver Package’ does not support a Task Sequence variable that allows you to set the Driver Package that you wish to apply to the OS. To use this step, you need one ‘Apply Driver Package’ action for each make/model/OS that you support in your environment! To make matters worse, you need to set a condition on each step so that it only runs on a supported model. For us, that means (at present) 75 separate task sequence steps with 75 separate condition statements. That’s a lot of pointing and clicking, and I am not going to do it.

Being the bull-headed guy that I am, I thought that I should be able to script this job out. Probably this was not the best use of my time this month, but I did it anyway, and below you can see the fruits of my labor, such as they are. Programming: the art of doing in 2000 lines of code and 20 hours of work what could have been done in two hours with a mouse and a reference guide.

So here is the UVM SCCM Driver Handling Solution, presented in six “easy parts”… First, we needed to get our drivers into SCCM. After wasting several hours with the SCCM PowerShell cmdlets (boo!), I found the following script which does just what I need using raw WMI calls under PowerShell:

http://blog.coretech.dk/kea/automate-importing-and-creating-drivers-packages-in-sccm-2012-r2/

Many thanks to the team at CoreTech for this code. I had to do some lite modifications to get the script to work in our site. The modified code is included in-line, below:

# From: http://blog.coretech.dk/kea/automate-importing-and-creating-drivers-packages-in-sccm-2012-r2/

# Imports drivers into SCCM from the directory specified in $sourceDir
# Creates driver packages in the directory specified in $packageDir
# The script does not use any MS-provided Configuration Manager PowerShell cmdlets, nor does it use any Configuration Manager DLL files/assemblies.
# It's all implemented in WMI, which is good because it bypasses several bugs in SCCM 2012 R2 CU3 that were preventing this process from working when we tried it with the CM PowerShell cmdlets!

# Drivers will be grouped into Administrative Categories and corresponding Driver Packages based on the folder structure of $sourceDir:

# root
#  |
#  +-> Win7
#  |    |
#  |    +- > Latitude E6500
#  |    +- > Optiplex 780
#  |
#  +-> WinPE
#       |
#	    +-> 5.0-x86
#       +-> 5.0-x64

# Will create the following Categories/Groups:
#  Win7-Latitude E6500
#  Win7-Optiplex 780
#  WinPE-5.0-x86
#  WinPE-5.0-x64

# The granularity of the category names can be changed by nesting more "get-childitem | foreach-object" loops into the function "SDS-ProcessFolder".
# Re-processing or existing folders can be forced by removing the "*.hash" files from the import source as follows:
# > Set-Location $sourceDir
# > gci -recurse -include *.hash | remove-item -force -confirm:$false

[string] $CMServer = "confman3"
[string] $SiteCode = "UVM"
[string] $sourceDir = "\\confman3\sources\drivers\import"
[string] $packageDir = "\\confman3\sources\drivers\packages"
[int] $currentDepth = 1

Function Clean-DriverDir {
    param ([string]$dir)
	# Clean up "cruft" files that lead to duplicate drivers in the share:
	Write-Custom "Cleaning extraneous files from $dir" -ForegroundColor Cyan
	$delItems = gci -recurse -Include version.txt,release.dat,cachescrubbed.txt,btpmwin.inf -LiteralPath $dir
	Write-Custom "Found " $delItems.count " files to delete..." -ForegroundColor Yellow
	if ($delItems.count -ne 0) {
		$delItems | remove-Item -force -confirm:$false
		$delItems = gci -recurse -Include version.txt,release.dat,cachescrubbed.txt,btpmwin.inf -LiteralPath $dir
		Write-Custom "New count for extraneous files: " $delItems.count -ForegroundColor Yellow
	}	
}

Function Get-SCCMDriverCategory
{
    [CmdletBinding()]
    PARAM
    (
        [Parameter(Position=1)] $categoryName
    )

    # Build the appropriate filter to return all categories or just one specified by name
    $filter = "CategoryTypeName = 'DriverCategories'"
    if ($categoryName -eq "" -or $categoryName -eq $null)
    {
        Write-Debug "Retriving all categories"
    }
    else
    {
        $filter += " and LocalizedCategoryInstanceName = '" + $categoryName + "'"
    }

    # Retrieve the matching list
    Get-SCCMObject SMS_CategoryInstance -filter $filter
}

Function New-SCCMDriverCategory
{
    [CmdletBinding()]
    PARAM
    (
        [Parameter(Position=1)] $categoryName
    )

    # Create a SMS_Category_LocalizedProperties instance
    $localizedClass = [wmiclass]"\\$sccmServer\$($sccmNamespace):SMS_Category_LocalizedProperties"

    # Populate the localized settings to be used with the new driver instance
    $localizedSetting = $localizedClass.psbase.CreateInstance()
    $localizedSetting.LocaleID =  1033 
    $localizedSetting.CategoryInstanceName = $categoryName
    [System.Management.ManagementObject[]] $localizedSettings += $localizedSetting

    # Create the unique ID
    $categoryGuid = [System.Guid]::NewGuid().ToString()
    $uniqueID = "DriverCategories:$categoryGuid"

    # Build the parameters for creating the collection
    $arguments = @{CategoryInstance_UniqueID = $uniqueID; LocalizedInformation = $localizedSettings; SourceSite = $sccmSiteCode; CategoryTypeName = 'DriverCategories'}

    # Create the new instance
    set-wmiinstance -class SMS_CategoryInstance -arguments $arguments -computername $sccmServer -namespace $sccmNamespace
}

Function New-SCCMDriverPackage
{
    [CmdletBinding()]
    PARAM
    (
        [Parameter(Position=1)] $name, 
        [Parameter(Position=2)] $description,
        [Parameter(Position=3)] $sourcePath
    )

    # Build the parameters for creating the collection
    $arguments = @{Name = $name; Description = $description; PkgSourceFlag = 2; PkgSourcePath = $sourcePath}
    $newPackage = set-wmiinstance -class SMS_DriverPackage -arguments $arguments -computername $sccmServer -namespace $sccmNamespace
    
    # Hack - for some reason without this we don't get the PackageID value
    $hack = $newPackage.PSBase | select * | out-null
    
    # Return the package
    $newPackage
}

Function New-SCCMFolder            
{            
  Param(                      
    $FolderName,
    $FolderType,            
    $ParentFolderID = 0
  )            
    
  If ($FolderType -eq "Device") { $FolderType = 5000 }
  If ($FolderType -eq "User") { $FolderType = 5001 }
                
  $SMSFolderClass = "SMS_ObjectContainerNode"             
  $Colon = ":"            
                    
  $WMIConnection = [WMIClass]"\\$sccmServer\$sccmNamespace$Colon$SMSFolderClass"            
  $CreateFolder = $WMIConnection.psbase.CreateInstance()            
  $CreateFolder.Name = $FolderName            
  $CreateFolder.ObjectType = $FolderType            
  $CreateFolder.ParentContainerNodeid = $ParentFolderID            
  $FolderResult = $CreateFolder.Put()
  
  $FolderID = $FolderResult.RelativePath.Substring($FolderResult.RelativePath.Length - 8, 8)
  
  $FolderID            
                
}

Function Move-SCCMObject            
{            
  Param(                    
    $SourceFolderID = 0,            
    $TargetFolderID,            
    $ObjectID,            
    $ObjectType           
  )
          
  If ($ObjectType -eq "Device") { $ObjectType = 5000 }
  If ($ObjectType -eq "User") { $ObjectType = 5001 }           
                      
  $Method = "MoveMembers"            
  $SMSObjectClass = "SMS_ObjectContainerItem"            
  $Colon = ":"            
                    
  $WMIConnection = [WMIClass]"\\$sccmServer\$sccmNamespace$Colon$SMSObjectClass"            
  $InParams = $WMIConnection.psbase.GetMethodParameters("MoveMembers")            
  $InParams.ContainerNodeID = $SourceFolderId            
  $InParams.InstanceKeys = $ObjectID           
  $InParams.ObjectType = $ObjectType            
  $InParams.TargetContainerNodeID = $TargetFolderID            
  $null = $WMIConnection.psbase.InvokeMethod($Method,$InParams,$null)
           
}

Function Get-ContentHash
{
    Param (
        $File,
        [ValidateSet("sha1","md5")]
        [string]$Algorithm="md5"
    )
	
    $content = "$($file.Name)$($file.Length)"
    $algo = [type]"System.Security.Cryptography.md5"
	$crypto = $algo::Create()
    $hash = [BitConverter]::ToString($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($content))).Replace("-", "")
    $hash
}

Function Get-FolderHash
{
    Param (
        [string]$Folder=$(throw("You must specify a folder to get the checksum of.")),
        [ValidateSet("sha1","md5")]
        [string]$Algorithm="md5"
    )
    
     $content = @()
	Get-ChildItem $Folder -Recurse -Exclude "*.hash" | % { $content += Get-ContentHash $_ $Algorithm }
   
    $algo = [type]"System.Security.Cryptography.$Algorithm"
	$crypto = $algo::Create()
	$hash = [BitConverter]::ToString($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($content))).Replace("-", "")
    
    $hash
}

Function Write-Custom($message, [System.ConsoleColor]$foregroundcolor)  
{  
	
	For ($i = 2; $i -le $currentDepth; $i++)
	{
		$tab += "`t"
	}
	
	$currentColor = $Host.UI.RawUI.ForegroundColor  
	if ($foregroundcolor)
	{
		$Host.UI.RawUI.ForegroundColor = $foregroundcolor
	}
	if ($message)  
	{  
		Write-Output "$($tab)$($message)"
	}  
	$Host.UI.RawUI.ForegroundColor = $currentColor 
}

Function Write-Headline($message)
{

	$dot = "------------------------------------------------------------------------------------------------------------"
	
	For ($i = 2; $i -le $currentDepth; $i++)
	{
		$dot = $dot.Substring(0, $dot.Length-8)
	}
	Write-Custom " "
	Write-Custom "$($dot)"
	Write-Custom "$($message)"
	Write-Custom "$($dot)"
}

Function New-SCCMConnection {

    [CmdletBinding()]
    PARAM
    (
        [Parameter(Position=1)] $serverName,
        [Parameter(Position=2)] $siteCode
    )


    # Clear the results from any previous execution

    Clear-Variable -name sccmServer -errorAction SilentlyContinue
    Clear-Variable -name sccmNamespace -errorAction SilentlyContinue
    Clear-Variable -name sccmSiteCode -errorAction SilentlyContinue
    Clear-Variable -name sccmConnection -errorAction SilentlyContinue


    # If the $serverName is not specified, use "."

    if ($serverName -eq $null -or $serverName -eq "")
    {
        $serverName = "."
    }


    # Get the pointer to the provider for the site code

    if ($siteCode -eq $null -or $siteCode -eq "")
    {
        Write-Verbose "Getting provider location for default site on server $serverName"
        $providerLocation = get-wmiobject -query "select * from SMS_ProviderLocation where ProviderForLocalSite = true" -namespace "root\sms" -computername $serverName -errorAction Stop
    }
    else
    {
        Write-Verbose "Getting provider location for site $siteName on server $serverName"
        $providerLocation = get-wmiobject -query "select * from SMS_ProviderLocation where SiteCode = '$siteCode'" -namespace "root\sms" -computername $serverName -errorAction Stop
    }


    # Split up the namespace path

    $parts = $providerLocation.NamespacePath -split "\\", 4
    Write-Verbose "Provider is located on $($providerLocation.Machine) in namespace $($parts[3])"
    $global:sccmServer = $providerLocation.Machine
    $global:sccmNamespace = $parts[3]
    $global:sccmSiteCode = $providerLocation.SiteCode


     # Make sure we can get a connection

    $global:sccmConnection = [wmi]"${providerLocation.NamespacePath}"
    Write-Verbose "Successfully connected to the specified provider"
}

function Get-SCCMObject {

    [CmdletBinding()]
    PARAM
    (
        [Parameter(Position=1)] $class, 
        [Parameter(Position=2)] $filter
    )

    if ($filter -eq $null -or $filter -eq "")
    {
        get-wmiobject -class $class -computername $sccmServer -namespace $sccmNamespace
    }
    else
    {
        get-wmiobject -query "select * from $class where $filter" -computername $sccmServer -namespace $sccmNamespace
    }
}


Function Import-SCCMDriverStore
{
	PARAM
    (
    [Parameter(Position=1)] $DriverStore,
    [Parameter(Position=3)] $CMPackageSource,
		#[Parameter(Position=4)] $PackageDepth,
		#[Parameter(Position=5)] $FolderDepth = ($packageDepth - 1),
		#[Parameter(Position=6)] $NameDepth = 1,
		[switch] $cleanup
    )
	
	
	if ($cleanup)
    {
		$currentPrincipal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent() )
		if (!$currentPrincipal.IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator ))
		{
			Write-Custom "You need to run Powershell as Administrator, to use the -Mirror switch." Red
			return;
		}
	
	}

	Write-Headline "Started Importing Driver Store: $($driverStore)"
	
	Get-ChildItem $driverStore | ? {$_.psIsContainer -eq $true} | % {
	
		$global:CurrentDepth = 1

		SDS-ProcessFolder $_
		
	
	}
	
}

Function SDS-ProcessFolder($path)
{
	$FolderPath = $path.FullName.Substring($DriverStore.Length+1, $path.FullName.Length-($DriverStore.Length+1))
	$FolderName = $path.FullName.Substring($DriverStore.Length+1, $path.FullName.Length-($DriverStore.Length+1))
	Write-Headline "Processing Folder: $($FolderName)"
	$CMFolderID = SDS-Folder $path 0
	Get-ChildItem $_.FullName | ? {$_.psIsContainer -eq $true} | % {
		$CurrentDepth = 2
		SDS-ProcessPackage $_ $FolderPath $CMFolderID
	}
}

Function SDS-Folder($folder, $parentID)  
{	
		
		$CMFolder = Get-SCCMObject -Class "SMS_ObjectContainerNode" -Filter "Name = `"$($folder.Name)`" AND ParentContainerNodeID = $($parentID) AND ObjectType = 23"
		
		If ($CMFolder)
		{
			$CMFolderID = $CMFolder.ContainerNodeID
		}
		Else
		{
			$CMFolderID = New-SCCMFolder -FolderName $folder.Name -FolderType 23 -ParentFolderID $parentID
			#Write-Custom "Created SCCM folder $($folder.Name) ($($SCCMFolderID))"
		}
		$CMFolderID
}


Function SDS-ProcessPackage($package, $folderPath, $folderID)
{
	$PackageName = $package.FullName.Substring($DriverStore.Length+1, $package.FullName.Length-($DriverStore.Length+1))
	
	#$PackageName = $PackageName.Substring($NameIndex+1, $PackageName.Length-($NameIndex+1))
	$PackageName = $PackageName.Replace("\","-")
	
	Write-Headline "Processing Driver Package: $($PackageName)"
	$PackageHash = Get-FolderHash $package.FullName
	If (Get-ChildItem $package.FullName -Filter "$($PackageHash).hash")
	{
		Write-Custom "No changes has been made to this Driver Package. Skipping." DarkGray
	}
	Else
	{
		#Cleanup the source directory to avoid import of duplicate drivers:
		Clean-DriverDir($package.FullName)
		
		$CMCategory = Get-SCCMDriverCategory -categoryName $PackageName
		if ($CMCategory -eq $null)
		{
			$CMCategory = New-SCCMDriverCategory $PackageName
			Write-Custom "Created new driver category $($PackageName)"
		}
		

		$CMPackageTrue = (get-wmiobject -query "Select * from SMS_DriverPackage join SMS_ObjectContainerItem ON SMS_ObjectContainerItem.InstanceKey = SMS_DriverPackage.PackageID WHERE SMS_ObjectContainerItem.ObjectType = 23 AND SMS_ObjectContainerItem.ContainerNodeID = `"$($folderID)`" AND SMS_DriverPackage.Name = `"$($PackageName)`"" -computername $sccmServer -namespace $sccmNamespace).SMS_DriverPackage
		if ($CMPackageTrue -eq $null) { $CMPackageTrue = get-wmiobject -query "Select * from SMS_DriverPackage join SMS_ObjectContainerItem ON SMS_ObjectContainerItem.InstanceKey = SMS_DriverPackage.PackageID WHERE SMS_ObjectContainerItem.ObjectType = 23 AND SMS_ObjectContainerItem.ContainerNodeID = `"$($folderID)`" AND SMS_DriverPackage.Name = `"$($PackageName)`"" -computername $sccmServer -namespace $sccmNamespace }
		$CMPackage = get-wmiobject -query "Select * from SMS_DriverPackage WHERE SMS_DriverPackage.PackageID = `"$($CMPackageTrue.PackageID)`"" -computername $sccmServer -namespace $sccmNamespace
		
		if ($CMPackage -eq $null)
		{
			Write-Custom "Creating new driver package $($PackageName)"
			$CMPackageSource = "$($CMPackageSource)\$($folderPath)\$($PackageName)"
			#$CMPackageSource = "$($CMPackageSource)\$($PackageName)"
			if (Test-Path $CMPackageSource)
				{
				if((Get-Item $CMPackageSource | %{$_.GetDirectories().Count + $_.GetFiles().Count}) -gt 0)
				{
					if ($cleanup)
					{
						Write-Custom "Folder already exists, removing content" Yellow
						dir $driverPackageSource | remove-item -recurse -force
					}
					else
					{
						Write-Custom "Folder already exists, remove it manually." Red
						return
					}
				}
			}
			else
			{
				$null = MkDir $CMPackageSource
			}
			
			$CMPackage = New-SCCMDriverPackage -name $PackageName -sourcePath $CMPackageSource
			Move-SCCMObject -TargetFolderID $folderID -ObjectID $CMPackage.PackageID -ObjectType 23
		}
		else
		{
			Write-Custom "Existing driver package $($PackageName) ($($CMPackage.PackageID)) retrieved." DarkGray
		}
		
		#$CurrentDepth += 1
		
		#$driverPackageContent = get-wmiobject -computername $sccmServer -namespace $sccmNamespace -query "SELECT * FROM SMS_Driver WHERE CI_ID IN (SELECT CTC.CI_ID FROM SMS_CIToContent AS CTC JOIN SMS_PackageToContent AS PTC ON CTC.ContentID=PTC.ContentID JOIN SMS_DriverPackage AS Pkg ON PTC.PackageID=Pkg.PackageID WHERE Pkg.PackageID='$($CMPackage.PackageID)')"
		#Get-ChildItem $package.FullName -Filter *.inf -recurse | Import-SCCMDriver -category $CMCategory -package $CMPackage | % {
		
		
		#}
		
		Get-ChildItem $package.FullName -Filter *.inf -recurse | % { SDS-ImportDriver $_ $CMCategory $CMPackage }
		
		Get-ChildItem $package.FullName -Filter "*.hash"  | Remove-Item 
		$null = New-Item "$($package.FullName)\$($PackageHash).hash" -type file 
	}
}

Function SDS-ImportDriver($dv, $category, $package)
{

		# Split the path
        $driverINF = split-path $dv.FullName -leaf 
        $driverPath = split-path $dv.FullName

        # Create the class objects needed
        $driverClass = [WmiClass]("\\$sccmServer\$($sccmNamespace):SMS_Driver")
        $localizedClass = [WmiClass]("\\$sccmServer\$($sccmNamespace):SMS_CI_LocalizedProperties")

        # Call the CreateFromINF method
        $driver = $null
        $InParams = $driverClass.psbase.GetMethodParameters("CreateFromINF")
        $InParams.DriverPath = $driverPath
        $InParams.INFFile = $driverINF
        try
        {
            $R = $driverClass.PSBase.InvokeMethod("CreateFromINF", $inParams, $Null)

            # Get the display name out of the result
            $driverXML = [XML]$R.Driver.SDMPackageXML
            $displayName = $driverXML.DesiredConfigurationDigest.Driver.Annotation.DisplayName.Text

            # Populate the localized settings to be used with the new driver instance
            $localizedSetting = $localizedClass.psbase.CreateInstance()
            $localizedSetting.LocaleID =  1033 
            $localizedSetting.DisplayName = $displayName
            $localizedSetting.Description = ""
            [System.Management.ManagementObject[]] $localizedSettings += $localizedSetting

            # Create a new driver instance (one tied to the right namespace) and copy the needed 
            # properties to it.
            $driver = $driverClass.CreateInstance()
            $driver.SDMPackageXML = $R.Driver.SDMPackageXML
            $driver.ContentSourcePath = $R.Driver.ContentSourcePath
            $driver.IsEnabled = $true
            $driver.LocalizedInformation = $localizedSettings
            $driver.CategoryInstance_UniqueIDs = @($category.CategoryInstance_UniqueID)

            # Put the driver instance
            $null = $driver.Put()
        
            Write-Custom "New Driver: $($displayName)"
        }
        catch [System.Management.Automation.MethodInvocationException]
        {
            $e = $_.Exception.GetBaseException()
            if ($e.ErrorInformation.ErrorCode -eq 183)
            {
                
                # Look for a match on the CI_UniqueID    
                $query = "select * from SMS_Driver where CI_UniqueID = '" + $e.ErrorInformation.ObjectInfo + "'"
                $driver = get-WMIObject -query $query.Replace("\", "/") -computername $sccmServer -namespace $sccmNamespace         
 
				Write-Custom "Duplicate Driver: $($driver.LocalizedDisplayName)" DarkGray
	
                # Set the category
                if (-not $driver)
                {
                    Write-Custom "`tUnable to import and no existing driver found." Yellow
                    return
                }
                elseif ($driver.CategoryInstance_UniqueIDs -contains $category.CategoryInstance_UniqueID)
                {
                    Write-Verbose "Existing driver is already in the specified category."
                }
                else
                {
                    $driver.CategoryInstance_UniqueIDs += $category.CategoryInstance_UniqueID
                    $null = $driver.Put()
                    Write-Verbose "Adding driver to category"
                }
            }
            else
            {
                Write-Custom "`tUnexpected error, skipping INF $($infFile): $($e.ErrorInformation.Description) $($e.ErrorInformation.ErrorCode)" Yellow
                return
            }
        }
        
        # Hack - for some reason without this we don't get the CollectionID value
		$hack = $driver.PSBase | select * | out-null

        # If a package was specified, add the driver to it
        if ($package -ne $null)
        {
			$driverPackageContent = get-wmiobject -computername $sccmServer -namespace $sccmNamespace -query "SELECT * FROM SMS_Driver WHERE CI_ID IN (SELECT CTC.CI_ID FROM SMS_CIToContent AS CTC JOIN SMS_PackageToContent AS PTC ON CTC.ContentID=PTC.ContentID JOIN SMS_DriverPackage AS Pkg ON PTC.PackageID=Pkg.PackageID WHERE Pkg.PackageID='$($package.PackageID)')"
            
			$doesDriverExist = $driverPackageContent | ? {$_.CI_UniqueID -eq $driver.CI_UniqueID}
			if ($doesDriverExist -eq $null)
			{
				# Add the driver to the package since it's not already there
				Write-Verbose "Adding driver to package"
				$null = Add-SCCMDriverPackageContent -package $package -driver $driver
			}

        }

        # Write the driver object to the pipeline
        #$driver

}

function Add-SCCMDriverPackageContent
{
    [CmdletBinding()]
    PARAM
    (
        [Parameter(Position=1)] $package,
        [Parameter(Position=2, ValueFromPipeline=$true)] $driver
    )

    Process
    {
        # Get the list of content IDs
        $idlist = @()
        $ci = $driver.CI_ID
        
        $i = 1
		$ids = Get-SCCMObject -class SMS_CIToContent -filter "CI_ID = '$ci'"

        if ($ids -eq $null)
        {
            Write-Warning "Warning: Driver not found in SMS_CIToContent"
        }
        foreach ($id in $ids)
        {
            $idlist += $id.ContentID
        }

        # Build a list of content source paths (one entry in the array)
        $sources = @($driver.ContentSourcePath)

        # Invoke the method
        try
        {
            $package.AddDriverContent($idlist, $sources, $false)
        }
        catch [System.Management.Automation.MethodInvocationException]
        {
            $e = $_.Exception.GetBaseException()
            if ($e.ErrorInformation.ErrorCode -eq 1078462229)
            {
                Write-Verbose "Driver is already in the driver package (possibly because there are multiple INFs in the same folder or the driver already was added from a different location): $($e.ErrorInformation.Description)"
            }
        }
    }

}

New-SCCMConnection $CMServer $SiteCode
Import-SCCMDriverStore $sourceDir $packageDir

#    This section formerly contained logic that has been moved to "Build-UDIInfoFiles.ps1",
#    which creates CSV files containing SCCM database info for use by UDI clients.

My version changes the import directory format a bit, adds some in-line documentation, and moves the local site variables to the top of the file. I also included a source-tree cleanup command that we used under MDT/LTI that removed useless Dell info files from the import structure. This helped reduce duplicate driver imports under LTI, but it may not be applicable under UDI.

Because of nuances in WordPress, I have had to split part 2 of this post into multiple pages. Driver scripting continues in part 2b:
http://blog.uvm.edu/jgm/2015/03/09/sccm-udi-2b-drivers/


Series Index:

Migrating to the SCCM UDI for OSD, part 1: Introduction

This post if the first installation in a series on migrating to Configuration Manager UDI from MDT “LiteTouch”.  Don’t know what I am talking about?  Well then, this blog series is likely of little interest to you.  (Hint:  This is all about deploying Windows operating systems using Microsoft’s own deployment technologies.)

“SCCM UDI for OSD”… sounds really cool, right?  Such snappy product names that we Windows Sys Admins get to work with!  For those not already bored to death, we are talking about the System Center Configuration Manager 2012 R2 User-Driven Installation for Operating System Deployment.  UDI is an optional extension to SCCM that is included in with free Microsoft “Solution Accelerator” called “MDT 2013” (The Microsoft Deployment Toolkit, 2013 edition).

Here at UVM, we have been using MDT in “LTI”, or “Lite Touch Installation” mode for many years (for those in the know, we used LTI back when MDT was called “BDD”, or the “Microsoft Solution Accelerator for Business Desktop Deployment”).  LTI has served up well for a long time.  We used MDT throughout the XP, Vista and Windows 7 lifecycle.  But since at least 2012 we have been wanting to migrate to the SCCM/UDI platform.  The initial driver for this migration was a desire to reduce the number of application installation packages that we need to maintain.  Currently we need to maintain packages in SCCM and in LTI.  By migrating to UDI, we can drop all of the LTI work.

In recent months, some additional pressures have come about which make this migration a bit more pressing:

  1. We would like to ensure that the SCCM management agent gets installed on all new computers at deployment time.  We have received complaints about the failure of LTI to configure the SCCM management agent.  While we feel that our current system is reliable, there still is a perception that SCCM agents are not getting installed on new computers.  The SCCM agent installation steps that are built into UDI task sequences should address this problem.
  2. OS Images in LTI often are out-of-date by 3-6 months.  In an effort to speed deployment times, we defer the application of OS updates at install time, and instead rely on the management agent to install updates in its own good time.  However, many support staff in the field do not like to release new computers without all updates already in place.  Using SCCM with UDI will help to address this problem in two ways:
    1. We can use SCCM to apply regular OS updates to our system images while they are offline.  This greatly reduces the number of updates that need to be applied to newly deployed computers.
    2. We then can force any remaining updates to run at deployment time without greatly increasing deployment time.

While all of this sounds very appealing, we also have a great deal of custom logic built into our current MDT/LTI environment.  Remapping our current workflows into UDI land is a difficult and time consuming task.

Additionally, while it is true that MDT/LTI and SCCM/UDI share a great deal of code, it is important to understand that they are very different things.  Many task sequence steps found in these tools look very similar and share nearly identical names.  However, these steps often are radicaly different in implementation.  Most notably, injection of drivers, installation of applications, and application of operating system images are handled in ways that utterly shattered our exiting task sequence logic.

The whole process of adapting MDT/LTI to SCCM/UDI was, at the very least, educational. I now know a lot more about programming SCCM than I ever wanted to know. I just wish that the techniques used here were useful elsewhere. I don’t work with any other Windows products that are so thoroughly rooted in WMI, so I have my doubts.

Lessons learned about programming in SCCM:

  1. Don’t even think about using the PowerShell cmdlets included with SCCM 2012 R2 (RTW-CU4). They are very buggy and feature incomplete.
  2. If you are an experienced C# programmer, you might consider using SCCM managed code to do your scripting work, but be forewarned that the aforementioned buggy cmdlets work off of these same managed code DLLs, so you might not have the best experience with them.
  3. For everyone else, you probably should stick to straight WMI calls using VBScript or PowerShell. I am trying to wean myself off of VBScript, so I chose to blaze new territory in programming SCCM using WMI programming with PowerShell. Call me crazy, but it was the only way I could get this stuff to work and stay sane (for a given value of sane).
  4. SCCM WMI objects that are not called using a fully-qualified object path do not have all object attributes exposed. Microsoft calls this “loose binding” in their documentation, but this is a misnomer. “Loose binding” should mean that the attributes are not exposed until they are used. In this case, it means that the attributes are totally empty and never will contain any data until you call a new version of the object using its $_.__PATH attribute.
  5. In order to update many/most SCCM objects, you need to call a generic WMI CLASS object for that object, and use the generic class to manipulate the actual WMI object. Confusing? Yes!

In the coming posts, I will document the scripts and procedures that I developed to remap our LTI logic into UDI logic.  It is going to be a bumpy road, so grab a fresh cup-o-joe in a spill-proof cup, put on your padded shorts, and fasten your seat belts.

Series Index:

Automated Driver Import in MDT 2013

As a follow up to my previous post, I also have developed a script to automate the import of drivers into MDT 2013.  This PowerShell script takes a source folder structure and duplicates the top two levels of folders in the MDT Deployment Share “Out-of-box drivers ” branch.  The script then imports all drivers found in the source directories to the matching folders in MDT.

All we have to do is extract all drivers for a given computer model into an appropriately named folder in the source directory, and then run the script.

################################################################################
#
#  Create-MDTDriverStructure.ps1
#  J. Greg Mackinnon, University of Vermont, 2013-11-05
#  Creates a folder structure in the "Out of Box Drivers" branch of a MDT 2013
#    deployment share.  The structure matches the first two subdirectories of 
#    the source filesystem defined in $srcRoot.  All drivers contained within
#    $srcRoot are imported into the deployment share.
#
#  Requires: 
#    $srcDir - A driver source directory, 
#    $MDTRoot - a MDT 2013 deployment share
#    - MDT 2013 must be installed in the path noted in $modDir!!!
#
################################################################################

# Define source driver directories:
[string] $srcRoot = 'E:\staging\drivers\import'
[string[]] $sources = gci -Attributes D $srcRoot | `
    Select-Object -Property name | % {$_.name.tostring()}
	
# Initialize MDT Working Environment:
[string] $MDTRoot = 'E:\DevRoot'
[string] $PSDriveName = 'DS100'
[string] $oobRoot = $PSDriveName + ":\Out-Of-Box Drivers"
[string] $modDir = `
	'C:\Program Files\Microsoft Deployment Toolkit\Bin\MicrosoftDeploymentToolkit.psd1'
Import-Module $modDir
New-PSDrive -Name "$PSDriveName" -PSProvider MDTProvider -Root $MDTRoot


foreach ($source in $sources){
    Write-Host "Working with source: " $source -ForegroundColor Magenta
    # Create the OOB Top-level folders:
    new-item -path $oobRoot -name $source -itemType "directory" -Verbose
    # Define a variable for the current working directory:
    $sub1 = $srcRoot + "\" + $source
    # Create an array containing the folders to be imported:
    $items = gci -Attributes D $sub1 | Select-Object -Property name | % {$_.name.tostring()}
    $oobDir = $oobRoot + "\" + $source

    foreach ($item in $items) {
		# Define source and target directories for driver import:
	    [string] $dstDir = $oobDir + "\" + $item
	    [string] $srcDir = $sub1 + "\" + $item
	
	    # Clean up "cruft" files that lead to duplicate drivers in the share:
		Write-Host "Processing $item" -ForeGroundColor Green
	    Write-Host "Cleaning extraneous files..." -ForegroundColor Cyan
        $delItems = gci -recurse -Include version.txt,release.dat,cachescrubbed.txt $srcDir
        Write-Host "Found " $delItems.count " files to delete..." -ForegroundColor Yellow
	    $delItems | remove-Item -force -confirm:$false
        $delItems = gci -recurse -Include version.txt,release.dat,cachescrubbed.txt $srcDir
        Write-Host "New count for extraneous files: " $delItems.count -ForegroundColor Yellow

	    # Create the target directory:
		Write-Host "Creating $item folder" -ForegroundColor Cyan
	    new-item -path $oobDir -name $item -itemType "directory" -Verbose
	
	    # Import all drivers from the source to the new target:
		Write-Host "Importing Drivers for $item" -ForegroundColor Cyan
	    Import-MDTDriver -Path $dstDir -SourcePath $srcDir 
		
        Write-Host "Moving to next directory..." -ForegroundColor Green
		
    } # End ForEach Item
} # End ForEach Source

Remove-PSDrive -Name "$PSDriveName"

 

Rethinking Driver Management in MDT 2013

We have been using the Microsoft Deployment Toolkit (MDT) in LTI/Lite Touch mode here at the University for a long time now.  Why, we used it to deploy XP back when MDT still was called the Business Desktop Deployment Solution Accelerator (BDD).  In this time, we have gone though several different driver management methods.  Gone are the nightmare days of dealing with OEMSETUP files, $OEM$ directories, can elaborate “DriverPack” injection scripts for XP (thank goodness).  

With the release of Vista, we moved from a PnP free-for-all model of driver detection.  After Windows 8.0, we found we really needed to separate our drivers by operating system.  Thus, we created Win7, Win8, and WinPE driver selection profiles.

But now we are finding that driver sprawl is becoming a major issue again.  On many new systems we run though a seemingly successful deployment, but end up with a non-responsive touch screen, a buggy track pad, and (sometimes) a very unstable system.

Starting this week, we are trying a new hybrid driver management approach.  We will create a driver folder for each computer model sold though our computer depot.  I have developed a custom bit of VBScript to check to see if the hardware being deployed to is a known model.  Driver injection will be restricted to this model if a match is found.  The script contains additional logic to detect support for both Windows 7 and Windows 8 variants, and to select the most current drivers detected.  Unknown models will fall back on the PnP free-for-all detection method.

Here is how it works…

  1. Create a new group in your OS deployment task sequence named “Custom Driver Inject”, or something similar.  Grouping all actions together will allow easier transfer of these custom actions to other Task Sequences:
    DM-DriverInjectGroup      
  2. Under this new group, add a new action of type “Set Task Sequence Variable”.  Name the variable “TargetOS”,and set the value to the OS that you are deploying from this task sequence.  You must follow the same naming convention that you use in your Out-of-box driver folder.  I use Win(X), where (X) is the major OS version of the drivers in the folder.  In this example, I have chose “Win8”:
    DM-SetTargetOS
  3. Add an action of type “Run Command Line”.  Name this action “Supported Model Check”.  Under the Command line field, enter “cscript “%SCRIPTROOT%\ZUVMCheckModel.wsf”.  (We will import this script into the deployment share later on.)
    DM-RunModelCheckScript
  4. Add a sub-group named “Supported Model Actions”.  Under the “Options” tab, add a condition of type “Task Sequence Variable”.  Use the variable “SupportedModel”, the Condition “equals”, and the Value “YES”.  (The SupportedModel variable gets set by the CheckModel script run in the previous step.):
    DM-ConditionalGroup
  5. Under this new group, add a new action of type “Set Task Sequence Variable”.  Name this task “Set Variable DriverGroup002”.  Under “Task Sequence Variable”, set “DriverGroup002”, and set the value to “Models\%TargetOS%\%Model%”.  (Note:  You could use “DriverGroup001”, but I already am using this variable to hold a default group of drivers that I want added to all systems.  The value “%TargetOS%\%Model%” defines the path to the driver group in the deployment share.  If you use a different folder structure, you will need to modify this path.):
    DM-SetDriverGroup
  6. Create a new task of type “Inject Drivers”.  Name this task “Inject Model-Specific Drivers”.  For the selection profile, select “Nothing”.  Be sure to select “Install all drivers from the selection profile”.  (NOTE: The dialog implies that we will be injecting only divers from a selection profile.  In fact, this step will inject drivers from any paths defined in any present “DriverGroupXXX” Task Sequence variables.)
    DM-InjectModelDrivers
  7. Now, under our original Custom Driver Inject group, add a new task of type “Inject Drivers”.  Choose from the selection profile “All Drivers”, or use a different fallback selection profile that suits the needs of your task sequence.  This time, select “Install only matching drivers from the selection profile”:
    DM-InjectUnsupported1
    Under the “Options” tab, add the condition where the “Task Sequence Variable” named “Supported Model” equals “NO”:
    DM-InjectUnsupported2
    This step will handle injection of matching drivers into hardware models for which we do not have a pre-defined driver group.
  8. Optionally, you now can open the “CustomSettings.ini” file and add the following to your “Default” section:
         DriverGroup001=Peripherals
    (I have a “Peripherals” driver group configured which contains USB Ethernet drivers used in our environment.  These are a necessity when deploying to hardware that does not have an embedded Ethernet port, such as the Dell XPS 12 and XPS 13.  You also could add common peripherals with complicated drivers such as a DisplayLink docking station or a Dell touch screen monitor.)
  9. Add the “ZUVMCheckMedia.wsf” script to the “Scripts” folder of your deployment share.  The code for this script is included below.  I think the script should be fairly easy to adapt for your environment.
  10. Finally, structure your “Out-of-Box Drivers” folder to contain a “Models” folder, and a single folder for each matching hardware model in your environment.  I get most of our driver collections from Dell:
    http://en.community.dell.com/techcenter/enterprise-client/w/wiki/2065.dell-driver-cab-files-for-enterprise-client-os-deployment.aspx
    (NOTE:  Thanks Dell!)
    The real challenge of maintaining this tree is in getting the model names right.  Use “wmic computersystem get model” to discover the model string for any new systems in your environment.  A table of a few current models I have been working with is included below.

Dell Marketing Model Name to WMI Name Translator Table:

  • Dell XPS 12 (first generation) – “XPS 12 9Q23”
  • Dell XPS 12 (second generation) – “XPS 12-9Q33”
  • Dell XPS 13 (first generation) – “Dell System XPS L321X”
  • Dell XPS 13 (second generation) – “Dell System XPS L322X”
  • Dell XPS 14 – “XPS L421Q”
  • Dell Latitude 10 – “Latitude 10 – ST2”
  • VMware Virtual Machine – “VMware Virtual Platform”
  • Microsoft Hyper-V Virtual Machine – “Virtual Machine”

A fun nuance we encountered last week was a Latitude E5430 model that contained “no-vPro” after the model number. Dell does not provide separate driver CABs for vPro/non-vPro models, so I added a regular expression test for Latitudes, and strip any cruft after the model number. There is one more problem down…

The following site contains a list of older model name translations:
http://www.faqshop.com/wp/misc/wmi/list-of-wmic-csproduct-get-name-results
As you can see, most Latitudes and Optiplexes follow sane and predictable model name conventions. I wish the same were true for the XPS.

Finally, I am indebted to the following sources for their generously detailed posts on driver management. Without their help, I doubt I would have been able to make this solution fly:

Jeff Hughes of the Windows Enterprise Support Server Core Team:
http://blogs.technet.com/b/askcore/archive/2013/05/09/how-to-manage-out-of-box-drivers-with-the-use-of-model-specific-driver-groups-in-microsoft-deployment-toolkit-2012-update-1.aspx

Andrew Barnes (aka Scriptimus Prime), whose posts on MDT driver management give the basics DriverGroups and model selection:
http://scriptimus.wordpress.com/2013/02/25/ltizti-deployments-injecting-drivers-during-deployment/
AND, of automating driver import into MDT (written for MDT 2012… some changes required for 2013):
http://scriptimus.wordpress.com/2012/06/08/mdt-2012-creating-a-driverstore-folder-structure/

The incredible Michael Neihaus, who in this post discusses the use of DriverGroups and Selection Profiles:
http://blogs.technet.com/b/mniehaus/archive/2009/09/09/mdt-2010-new-feature-19-improved-driver-management.aspx

And finally Eric Schloss of the “Admin Nexus”, who give me the idea of developing a fallback for systems that do not match a known model. It was this key bit of smarts that gave me the confidence to move forward with a model-specific driver grouping strategy:
http://adminnexus.blogspot.com/2012/08/yet-another-approach-to-driver.html

ZUVMCheckModel.wsf script:

(NOTE: WordPress stripped off the WSF headers and footers from my script. These are the first three and last two lines in the script. If you copy from this post, please remember to place greater than and less than tags around these lines before running, as indicated in the comments.)

' Uncomment and wrap each of the following three lines in less than/greater than characters to convert them to tags.
'job id="ZUVMCheckModel"
'script language="VBScript" src="ZTIUtility.vbs"/
'script language="VBScript"

Option Explicit
RunNewInstance

'//--------------------------------------------------------
'// Main Class
'//--------------------------------------------------------
Class ZUVMCheckModel
	
	'//—————————————————————————-
	'//  Constructor to initialize needed global objects
	'//—————————————————————————-
	Private Sub Class_Initialize
	End Sub
	
	'//--------------------------------------------------------
	'// Main routine
	'//--------------------------------------------------------

	Function Main()
	' //*******************************************************
	' //
	' // File: ZTIUVMCheckModel.wsf
	' //
	' // Purpose: Checks the model of this system against
	' //          a list of known machine models.  Returns
	' //          TRUE if a matching model is detected.
	' //
	' // Usage: cscript ZUVMCheckModel.wsf /Model: [/debug:true]
	' //
	' //*******************************************************
	
	'Use the following lines for debugging only.
	'oEnvironment.Item("TargetOS") = "Win7"
	'oEnvironment.item("DeployRoot") = "c:\local\mdt"
	'oEnvironment.Item("Model") = "Latitude E6500 some annoying variation"
	'End debug Params

	  Dim aModels()          'Array of models taken from DriverGroups.xml
	  Dim bOldDrivers        'Boolean indicating drivers present for an older OS version
	  Dim i                  'Generic integer for looping
	  Dim j                  'Generic integer for looping
	  Dim iRetVal            'Return code variable
	  Dim iMaxOS             'Integer representing the highest matching OS driver store
	  Dim oRegEx
	  Dim oMatch
	  Dim match
	  Dim oXMLDoc            'XML Document Object, for reading DriverGroups.xml
	  Dim Root,NodeList,Elem 'Objects in support of oXMLdoc
	  Dim sDGPath            'Path to DriverGroups.xml file
	  Dim sInitModel         'String representing the initial value of
	                         '   oEnvironment.Item("Model")
	  Dim sItem	             'Item in aModels array.
	  Dim sMaxOS             'OS Name of highest matching OS driver store
	  Dim sOSFound           'OS Name for a given matching driver set.
	  
	  oLogging.CreateEntry "Begin ZUVMCheckModel...", LogTypeInfo
	  
	  'Set the default values:
	  oEnvironment.Item("SupportedModel") = "NO"
	  iMaxOS = CInt(Right(oEnvironment.Item("TargetOS"),1))
	  'wscript.echo "Default value for iMaxOS = " & iMaxOS
	  bOldDrivers = false
	  sInitModel = oEnvironment.Item("Model")
	  'wscript.echo "sInitModel value = " & sInitModel
	  
	  Set oRegEx = New RegExp
	  oRegEx.Global = True
	  oRegEx.IgnoreCase = True
	  
	  'Modify the detected model name to handle known variations:
	  oRegEx.pattern = "Latitude"
	  if oRegEx.test(sInitModel) then
		oLogging.CreateEntry "Model is a Latitude.  Cleaning up the model name...", LogTypeInfo
		oRegEx.pattern = " "
		set oMatch = oRegEx.Execute(sInitModel)
		'wscript.echo "oMatch Count is: " & oMatch.count
		if oMatch.Count > 1 then
			i = oMatch.item(1).FirstIndex
			oEnvironment.Item("Model") = Left(sInitModel,i)
			'wscript.echo """"&oEnvironment.Item("Model")&""""
		end if
	  end if

	  'Check for DriverGroups.xml file, which will contain the supported model list:
	  iRetVal = Failure
	  iRetVal = oUtility.FindFile("DriverGroups.xml", sDGPath)
	  if iRetVal  Success then
		oLogging.CreateEntry "DriverGroups file not found. ", LogTypeError
		exit function
	  end if 
	  oLogging.CreateEntry "Path to DriverGroups.xml: " & sDGPath, LogTypeInfo
	  
	  'Parse the DriverGroups.xml file:
	  oLogging.CreateEntry "Parsing DriverGroups.xml...", LogTypeInfo
	  Set oXMLDoc = CreateObject("Msxml2.DOMDocument") 
	  oXMLDoc.setProperty "SelectionLanguage", "XPath"
	  oXMLDoc.load(sDGPath)
	  Set Root = oXMLDoc.documentElement 
	  Set NodeList = Root.getElementsByTagName("Name")
	  oLogging.CreateEntry "NodeList Member Count is: " & NodeList.length, LogTypeInfo
	  'oLogging.CreateEntry "NodeList.Length variant type is: " & TypeName(NodeList.Length), LogTypeInfo
	  i = CInt(NodeList.length) - 1
	  ReDim aModels(i) 'Resize aModels to hold all matching DriverGroup items.
	  'oLogging.CreateEntry "List of Available Driver Groups:", LogTypeInfo
	  i = 0
	  For Each Elem In NodeList
		if InStr(Elem.Text,"Models\") then
			aModels(i) = Mid(Elem.Text,8)	'Add text after "Models\"
			'oLogging.CreateEntry aModels(i), LogTypeInfo
			i = i + 1
		end if
	  Next
	  oLogging.CreateEntry "End Parsing DriverGroups.xml.", LogTypeInfo

	  'Loop through the list of supported models to find a match:
	  oLogging.CreateEntry "Checking discovered driver groups for match to: " & oenvironment.Item("Model"), LogTypeInfo
	  For Each sItem in aModels
		oLogging.CreateEntry "Checking Driver Group: " & sItem, LogTypeInfo
		i = InStr(1, sItem, oEnvironment.Item("Model"), vbTextCompare)

		'wscript.echo TypeName(i) 'i is a "Long" number type.
		If i  0 Then
			oLogging.CreateEntry "Matching Model found.", LogTypeInfo
			
			j = InStr(sItem,"\")
			sOSFound = Left(sItem,j-1)
			'wscript.echo "sOSFound = " & sOSFound 
			if (InStr(1,sOSFound,oEnvironment.Item("TargetOS"),vbTextCompare)  0) then
				oLogging.CreateEntry "Drivers matching the requested OS are available.  Exiting with success.", LogTypeInfo
				oEnvironment.Item("SupportedModel") = "YES"
				iRetVal = Success
				Main = iRetVal
				Exit Function
			end if
			if iMaxOS > CInt(Right(sOSFound,1)) then
				iMaxOS = CInt(Right(sOSFound,1))
				'wscript.echo "iMaxOS = " & iMaxOS
				sMaxOS = sOSFound
				bOldDrivers = true
				'wscript.echo "sMaxOS = " & sMaxOS
			end if
		End If
	  Next
		
	  If bOldDrivers Then 'Run if sMaxOS is defined... set a boolean when this is defined and test against that?
		oLogging.CreateEntry "Model drivers were found for an OS older than the one selected...", LogTypeWarning
		oEnvironment.Item("SupportedModel") = "YES"
		oEnvironment.Item("TargetOS") = sMaxOS
	  Else
	    oLogging.CreateEntry "No matching drivers were found for this model.", LogTypeInfo
	  End If
	  
	  oLogging.CreateEntry "End ZUVMCheckModel.", LogTypeInfo

	  iRetVal = Success
	  Main = iRetVal

	End Function

End Class

' Uncomment and wrap each of the following two lines in less than/greater than characters to convert them to tags.
'/script
'/job

LiteTouch failures on UEFI systems

Today I got a complaint that a user could not deploy Windows 8 on a UEFI-enabled computer (a Latitude 10 tablet, to be precise) using our MDT 2012 Update 1 deployment share (in Lite Touch mode, of course).  At the same time, I was experiencing problems deploying Windows 8 on a Dell XPS 14 with UEFI firmware enabled.  Interestingly, the deployment problem did not occur when running with Legacy BIOS enabled.  What gives?

The error reported by Lite Touch was: “MDT FAILURE (5616): Verify BCDbootex”.  Much digging though the logs revealed the command line that triggered the problem (It was spelled out in the “LTIApply.log”).  This command was something like:
\\server\deployShare\Tools\x64\bcdboot c:\windows /l en-us /s v: /f UEFI

If I try to run the command above manually, I just get back the bcdboot.exe help dialog.  Further investigation revealed that the version of bcdboot.exe on the server share is dated to 2009.  This is the Windows 7 RTM release version!

I created a new deployment share to see if the bad version of bcdboot.exe gets placed there again… it did not.  So, I removed bcdboot.exe from the deployment share (along with an old version of ImageX.exe and some other expired utilities).  Upon re-running LiteTouch, deployment succeeds.  Both Windows 7 and Windows 8 boot media contain bcdboot.exe in the search path… no server side copy is required.  I wonder how it got there in the first place?  Maybe we were trying to make it easier for people to capture system images using the LiteTouch boot media.  Another entry for the department of unintended consequences…

Dell XPS 12 – The Windows 8 Flagship?

Regular readers of my blog (all two of you) may recall the “series” I started this fall on Windows 8 launch devices (concerning the HP Envy X2 and the Samsung SmartPC Pro 700t). These devices both had strengths, but failed in other ways that made them difficult or impossible to support in an enterprise environment. This month, I got my hands on a device that breaks though that barrier and satisfies in a big way. The new Dell XPS 12 finally arrived on our campus about two weeks ago. We immediately were taken with its light weight (3 lbs.), sleek styling, and novel materials (full carbon fiber base, carbon fiber and aluminum lid, and that unique flip-over touch screen). The 8-second boot time is another impressive feature. A longer battery life would have been appreciated, but I can live with it. Other helpful enhancements would be the inclusion of an active stylus. I also would appreciate slightly more resistance in the keyboard.

Others have weighed in on the appearance, performance, and usability of this fancy Ultrabook, though, so I will forgo further commentary on those aspects of the XPS 12. What most concerned us was the ability to support OS redeployment, BitLocker encryption, and hardware servicing on our Campus.

We unboxed and re-deployed the computer with Windows 8 Enterprise within one day. There were a few deployment hiccoughs, but in general re-deployment was what we have come to expect from Dell. All required drivers for the XPS 12 were made available in a single downloadable CAB file. We extracted this CAB to our MDT/LiteTouch Deployment Share, rebuilt our boot media, and initiated a LiteTouch deployment. There was a brief problem getting LiteTouch to start… we needed to disable the “Safe Boot” option in EFI/BIOS, and we needed to set the EFI boot mode to “Legacy” to allow our boot media to operate. Once those changes were made, the XPS 12 booted to our USB WinPE media without complaint. Upon completion of deployment, all devices in the device manager reported as functioning. There were no “poorly-behaved” drivers that required un-scripted installation. We did find that the track-pad was behaving strangely. Investigation revealed that the PnP process had grabbed a Windows 7 track-pad driver from our deployment share. We corrected this manually, then separated our Windows 8 drivers from our Windows 7 drivers in the Deployment Workbench… this should prevent the problem from recurring in future deployments.

BitLocker was easy to implement. The TPM chip readily was recognized by the OS, and TPM-with-PIN encryption was accomplished in minutes. I spent half a day trying to encrypt an older Dell Latitude E6500 a few months back. This was a breeze by comparison.

On the servicing front, we have good news. Dell now is allowing on-site servicing for all XPS models, with full reimbursement for parts and labor for qualified technicians. Physical serviceability is a big concern for newer Ultrabooks. A troubling trend in tablet and notebook design is the use of solder on drive mounts and glue to hold batteries in place (the latest “Retina” MacBooks and the MS Surface tablets suffer from these problems). Fortunately, it appears that all major components of the XPS 12 can be removed and replaced without the need to re-solder or remove glue. The most frequently swapped components such as the battery, mSATA drive, and memory chips look pretty easy to access. The keyboard is a bit of a pain to get to, but at least it can be serviced.

If only more Windows 8 launch products had been this good… I hope we see more products of this quality coming from Dell (and other vendors) in the near future.

Update:  2013-11-1

Five months into using the XPS 12, I started to have trouble with the trackpad.  It would not click anymore!  Since we are working with an evaluation unit, I do not have warranty coverage, so I figured I had no warranty to void by attempting to repair it on my own.

Some digging in the Dell support site revealed that the so-called XPS 12 “User Manual” is actually a service manual!  The readily available PDF document illustrates step-by step how to remove the carbon fiber base plate and the battery in order to get to the track pad.  (The only challenging part was locating a #5 Torx screwdriver to take off the base plate.)  Within 15 minutes I had removed the click pad, and cleaned the trapped grit out from under it.  (Within a half hour I had the unit re-assembled.  In another 15 minutes I had taken the base plate back off, reconnected the battery power connector, and re-attached the base plate, again.)  The unit powered back on as normal, with the track pad working like new.

At a time when consumer devices are moving towards non-serviceable designs (think MacBook Retina), it is nice to see a device that is thin and light while still maintaining serviceability.  Perhaps the track pad on the MacBook Retina is less prone to trapping grit, but imagine if it did?  With all the components glued together, you might be out $2000 because of a bit of sand.  I really have to hand it to Dell.  These XPS Ultrabooks are really nicely engineered.

 

Evaluating Windows 8 Tablets – Samsung ATIV SmartPC Pro (700T)

The journey continues…

The boss approved purchase of a Samsung ATIV SmartPC Pro (the “700T” model).  I wassoooexcited… this was the tablet PC I had been waiting for.  Thin, light, and fully convertible from Ultrabook to slate.  Stylus included, 1080 high-definition display, full Intel i5 processor.  So much to love…

First impressions were really positive.  The build quality seemed really high… solid magnesium case, good keyboard response, fast boot, very responsive Wacom digitizer stylus.  As a tablet, this thing is awesome. And while it is expensize compared to an iPad, it is very cheap compared to the Tablet PCs of yesteryear.

However, I quickly ran into trouble.  When typing with the SmartPC on my lap, the keyboard would frequently disconnect from the display.  It would not fall off, but the tablet component would lose electrical connection to the keyboard, causing typing input to stop.  Sometimes this would happen as often as five times in a single line of text.  Awful!

There were other problems as well.  Like the HP Envy X2, the screen does not tilt back far enough to allow comfortable use of the keyboard on a countertop.  The 1080p display, which is very crisp and bright, is inconvenient to use for remote desktop connections to Server 2008 R2 and earlier hosts (the fonts do not scale for remote desktop sessions, leading to comically tiny print size and rediculiously small buttons and window controls).  The system did not include a TPM chip (that is only available on the models that ship with Win8 Pro… something that was not clear when ordering the device).  And finally, Samsung does not bundle drivers for the SmartPC in any way that is convenient for business deployment.  Re-imaging the systems would be a pain.

It also is worth noting that Microsoft decided that in-place upgrades of retail versions of Win 8 to volume license editions woudl not be supported.  If you want simply to install Win 8 Enterprise over the factory-shipped consumer edition of Win8, you are out of luck.  I also experienced this problem with the HP Envy X2.  For corporate users, volume license installs are strictly a nuke-and-repave operation.  Booooooo!  This is not Samsung’s fault, but the lack of support for business deployment (i.e. driver bundles or driver repository building tools) is a killer for the SmartPC in the enterprise.

I really wanted to love this device, but I really just have to return it.  Consumers seeking a top-performance tablet may love it, but it does not work for this sysadmin.  I am hoping that the Lenovo ThinkPad Helix will work out better.

Evaluating Windows 8 Tablets in the Enterprise – HP Envy X2

In desperation over our inability to tell University employees what they should be looking for in a Windows 8 tablet, I asked the boss if we could get our hands on one of the new Intel “Atom” processor-based Windows 8 tablets (these are the “Clover Trail” Atom processors, designed to compete with ARM-based devices).  I had been wanting to eval a Samsung ATIV Smart PC 500T, but these have been hard to get locally.  Instead, I bought a hot-off-the-shelf HP Envy X2.  This device boasts a well-engineered all-metal shell, full size keyboard dock with full-sized HDMI and SD card readers, and an extra battery in the dock for a claimed 15-hour run life.  It also claims to support an optional digitizer stylus.

I only have just started putting the machine though its paces.  My first impression is that it performs surprisingly well as a standard notebook, but that there will be significant challenges in supporting these types of devices at the same level as our existing business-model Dell systems.  I am not going to bother “reviewing” this tablet… others in the trade can handle that.  Rather, this blog post is going to address the challenges of supporting a consumer tablet in a business environment.

  • Processor:  The new “Clover Trail” Atom processors are 32-bit only.  Surprise!  I though the industry was leaving 32-bt behind, but it appears to be alive and well.  We had made the initial decision to support only 64-bit Windows 8, and have developed only 64-bit baseline images.  I see that this choice will need to be reconsidered.
  • EFI/UEFI:  These new systems boot using EFI, with emulated BIOS, with the “SafeBoot” option enabled.  Out of the box, you cannot boot to USB because the SafeBoot prohibits this.  You need to load your OS to change EFI options.  EFI is not identical between systems, so navigating the process of booting to deployment/maintenance media will be a tough challenge for technicians to work through.  I actually was completely unable to boot the Envy X2 to an USB flash drive, running either WinPE (MDT boot media) or the FreeDOS-based(?) CloneZilla  live CD.  Bummer.
  • Drivers:  Most new tablets are aimed at the consumer market.  As a result, the vendors make little effort to package drivers in a way that is convenient for local IT staff to integrate into on-premise Windows deployment tools.  The Envy X2 is no exception.  A small handful of one-off driver installers are available, including a big bundle of Intel Chipset drivers.  The chipset drivers were critical in getting a freshly installed Windows 8 Enterprise OS working with the hardware.
  • Windows Editions:  This tablet shipped with “Windows 8”.  Not “Home”, not “Professional”, not “Ultimate”.  I tried performing an in-place SKU upgrade to Windows 8 Enterprise, but setup.exe said that this was not supported, so I needed to do a full OS install.  This process worked, but it was seriously aggravating to have to boot to the OEM OS, start the Enterprise OS install, re-install all of the required drivers, then clean up the original OS install.  Our users will not want to have to deal with this, and it will make our IT support staff very tired.
  • Hardware:  No Ethernet.  Unfortunately, our MDT/LTI deployment tools are designed to run over Ethernet, not Wi-Fi.  The LTI scripts actually will terminate if a Wi-Fi connection is detected.  Of course, application-only LTI task sequences really should run just fine over wireless, but the scripts still will not run over wireless.  We either will have to comment out the Wi-Fi checks, or require that the person launching LTI have a USB Ethernet dongle handy.

So… a lot of challenges.  More details as time permits.

Oh, one small “review style” note.  I decided to evaluate this tablet because HP claims that it supports an optional stylus.  However, the stylus for this device is not actually available for sale at this time.  Further, the device does not use the common Wacom or N-Trig digitizers, so buying a spare “Bamboo” stylus will not help you here.  HP has chosen to use the new “Atmel” integrated touch/pen sensors, and as such an Atmel-compatible stylus is required.  I cannot find these on the market anywhere.  As a result I cannot make any recommendation for or against the purchase of this device for Tablet PC enthusiasts.  I don’t even know if the stylus will be available for sale before the return period for this device expires.

UPDATE:

I returned this tablet.  Why?  It was not the screen resolution, which I though would be a problem but was not.  There were three primary reasons:

  1. It was not possible to determine the quality of the digitizer within the return period of the tablet.  I was unwilling to accept the risk of having a low-quality stylus for note taking.
  2. Keyboard dock quality was low.  The keyboard itself was reasonably good, but the trackpad was very annoying.  The texture was awful, and it was overly sensitive to the slightest palm brushes.  Given the small size of the keyboard deck, it was impossible to avoid brushing the trackpad, too.  Also, the screen did not tip back far enough for comfort when used on a countertop or other waist-height surface.
  3. Business deployment essentially was unsupportable.  HP support could not assist me with initialization of the TPM chip for BitLocker.  It appeared that a TPM was present, but there was no option in BIOS to reset the TPM, and the OS could not get ownership of the chip.  Also, the total lack of driver bundles would make deployment using MDT very difficult.
  4. The graphics card could not drive my external display at native resolution.  It maxed out at 1080p.

I did quite like this tablet, though.  Consumers seeking an additional computer for the road may really enjoy using it.  For fussy power users like me, it was close butnot quite there.