Tag Archives: SCCM

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:

Bulk-modification of deployment deadlines in SCCM

A full two years into testing we finally are moving forward with production deployment of our Configuration Manager 2012 (SCCM) environment. Last month we (recklessly?) migrated 1000 workstations into the environment. While the deployment was a technological success, it was a bit of black-eye in the PR department.

Client computers almost uniformly did an unplanned reboot one hour after the SCCM agent got installed on their workstations. In addition to that, many clients experienced multiple reboot requests over the coming days. Many client reported that they did not get the planned 90-minute impending reboot warning, but only the 15-minute countdown timer.

Lots of changes were required to address this situation:

See the “Suppress and required client restarts” setting documented here:
http://technet.microsoft.com/en-us/library/gg682067.aspx#BKMK_EndpointProtectionDeviceSettings
This one was causing clients to reboot following upgrade of their existing Forefront Endpoint Protection client to SCEP. That explained the unexpected 60-minute post-install reboot.

Next, we decided to change the reboot-after deadline grace period from 90 minutes to 9 hours, with the final warning now set to one hour, up from 15 minutes. This should allow people to complete work tasks without having updates interrupt their work day.

Finally, we are planning to reset the deployment deadline for all existing software update deployments to a time several days out from the initial client installation time. Since we have several dozen existing software update group deployments, we need a programmatic approach to completing this task. The key to this was found here:
http://www.scconfigmgr.com/2013/12/01/modify-the-deadline-time-of-an-adr-deployment-with-powershell/
Thanks to Nickolaj Andersen for posting this valuable script.

It did take me a bit of time to decode what Nickolaj was doing with his script (I was not already familiar with the Date/Time format generally used in WMI). I modified the code to set existing update group deployments to a fixed date and time provided by input parameters. I also added some in-line documentation to the script, and added a few more input validation checks:

# Set-CMDeploymentDeadlines script
#   J. Greg Mackinnon, 2014-02-07
#   Updates all existing software update deployments with a new enforcement deadline.
#   Requires specification of: 
#    -SiteServer (an SCCM Site Server name)
#    -SiteCode   (an SCCM Site Code)
#    -DeadlineDate
#    -DeadlineTime
#

[CmdletBinding()]

param(
    [parameter(Mandatory=$true)]
    [string] $SiteServer,

    [parameter(Mandatory=$true)]
    [string] $SiteCode,

    [parameter(Mandatory=$true)]
    [ValidateScript({
        if (($_.Length -eq 8) -and ($_ -notmatch '[a-zA-Z]+')) {
            $true
        } else {
            Throw '-Deadline must be a date string in the format "YYYYMMDD"'
        }
    })]
    [string] $DeadlineDate,

    [parameter(Mandatory=$true)]
    [ValidateScript({
        if (($_.Length -eq 4) -and ($_ -notmatch '[a-zA-Z]+')) {
            $true
        } else {
            Throw '-DeadlineTime must be a time string in the format "HHMM", using 24-hour syntax' 
        }
    })]
    [string] $DeadlineTime
)

Set-PSDebug -Strict

# WMI Date format is required here.  See:
# http://technet.microsoft.com/en-us/library/ee156576.aspx
# This is the "UTC Date-Time Format", sometimes called "dtm Format", and referenced in .NET as "dmtfDateTime"
#YYYYMMDDHHMMSS.000000+MMM
#The grouping of six zeros represents milliseconds.  The last cluster of MMM is minute offset from GMT.  
#Wildcards can be used to for parts of the date that are not specified.  In this case, we will not specify
#the GMT offset, thus using "local time".

# Build new deadline date in WMI Date format:
[string] $newDeadline = $DeadlineDate + $DeadlineTime + '00.000000+***'
Write-Verbose "Time to be sent to the Deployment Object: $newDeadline"
 

# Get all current Software Update Group Deployments.
# Note: We use the WMI class "SMS_UpdateGroupAssignment", documented here:
# http://msdn.microsoft.com/en-us/library/hh949604.aspx
# Shares many properties with "SMS_CIAssignmentBaseClass", documented here:
# http://msdn.microsoft.com/en-us/library/hh949014.aspx 
$ADRClientDeployment = @()
$ADRClientDeployment = Get-WmiObject -Namespace "root\sms\site_$($SiteCode)" -Class SMS_UpdateGroupAssignment -ComputerName $SiteServer

# Loop through the assignments setting the new EnforcementDeadline, 
# and commit the change with the Put() method common to all WMI Classes:
# http://msdn.microsoft.com/en-us/library/aa391455(v=vs.85).aspx
  
foreach ($Deployment in $ADRClientDeployment) {

    $DeploymentName = $Deployment.AssignmentName

    Write-Output "Deployment to be modified: `n$($DeploymentName)"
    try {
        $Deployment.EnforcementDeadline = "$newDeadline"
        $Deployment.Put() | Out-Null
        if ($?) {
            Write-Output "`nSuccessfully modified deployment`n"
        }
    }
    catch {
        Write-Output "`nERROR: $($_.Exception.Message)"
    }
}

We additionally could push out the deployment time for Application updates as well using the “SMS_ApplicationAssignment” WMI class:
http://msdn.microsoft.com/en-us/library/hh949469.aspx

In this case, we would want to change the “UpdateDeadline” property, since we do not set a “deployment deadline” for these updates, but instead are using application supersedence rules to control when the updates are deployed.

Prepare SCCM Clients for Cloning

Annoying task… configuring VMware View desktops for use in an environment that utilizes VMware View.  Some say, don’t put the management agent on the View desktop, just rebuild your desktops every time there is a security patch.  I say, even if recomposing your pools is fast and easy, I still do not want to do it with every patch release.

Best practice for preparing a reference computer for deployment in an SCCM environment is to not include the SCCM client.  However, SCCM client installation is SLOW, so I would like to save time and CPU load by including the software.  Documentation how to do this is sketchy.  Also, I really want a script fired off by the QuickPrep process to do the prep work, so that someone does not forget that it needs to be done.

Here is my first pass at the script… again, not too pretty, but functional:

'==========================================================================
'
'  NAME:    sccmClientPrep.vbs
'
'  AUTHOR:  J. Greg Mackinnon
'  DATE:    2013-02-01
'
'  COMMENT: prepares SCCM client for cloning
'           Requires:
'             - Certutil.exe in %systemroot%\system32 (included with Win7)
'           Returns:
'           1 - CCMEXEC service stop failure
'           2 - Machine Certificate Store deletion failure
'           4 - SMS Certificate Store deletion failure
'           8 - SMSCFG.INI deltion failure
'==========================================================================
option explicit

'=-=-=-=-=-=-=-=-=-=-=-=-=
'        CONSTANTS
const MACH_STORE = "My"
const SMS_STORE = "SMS"
const SVCNAME = "ccmexec"
const TIMEOUT = "120"

'=-=-=-=-=-=-=-=-=-=-=-=-=
'        OBJECTS
dim oShell
set oShell = CreateObject("WScript.Shell")

'=-=-=-=-=-=-=-=-=-=-=-=-=
'        VARIABLES
dim sSysRoot,sCUPath,sINIPath
dim iRet, iExit

sSysRoot = oShell.ExpandEnvironmentStrings("%SystemRoot%")
sCUPath = sSysRoot & "\system32\certutil.exe"
sINIPath = sSysRoot & "\SMSCFG.INI"
iExit = 0

'=-=-=-=-=-=-=-=-=-=-=-=-=
'   FUNCTIONS AND SUBS
function stopSvc(sSvcName,iTimeout)
' Stops the Windows service with name matching input string "sSvcName".
' Times out in iTimeout seconds.
' Needs routine to verify that sSvcName is a valid service name.
	'Variables:
	dim bDone 
	dim iSecs 
	bDone = False
	iSecs = 0

	'Objects and Collections:
	dim cSvcs
	dim oWMI, oSvc
	Set oWMI = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
	Set cSvcs = oWMI.ExecQuery("Select * from Win32_Service Where Name = '" & sSvcName & "'")

	'Stop the service if it is running, exit if it is not running
	For Each oSvc In cSvcs
		if oSvc.State = "Running" then
			oSvc.StopService
		else
			stopSvc = 0
			exit function
		end if
		exit for 'Only on service in collection
	Next

	'Check on the service until stopped.  Timeout in iTimeout seconds.
	Do while bDone = False
		Set cSvcs = oWMI.ExecQuery("Select * from Win32_Service Where Name = '" & sSvcName & "'")
		bDone = True
		For Each oSvc In cSvcs
			If oSvc.State  "Stopped" Then
				bDone = False
				WScript.Sleep 1000
				Exit For
			End If
		Next
		iSecs = iSecs + 1
		If iSecs >= iTimeout Then
			stopSvc = 1
			exit function
		End If
	Loop 
	stopSvc = 0
end function

function delCert(sStore,sSerial)
'Deletes certificate in certificate store "sStore" with serial number "sSerial"
'Returns: The ExitCode from certutil.exe
'Requires: 
'   - WScript.Shell object named "oShell"
'   - Defined path to "certutil.exe" named "sCUPath"
'   - Presence of certutil.exe on the system
	dim oExec, oStdOut
	dim sLine
	
	'wscript.echo "About to execute: " & sCUPath & " -delstore " & sStore & " " & sSerial
	set oExec = oShell.Exec(sCUPath & " -delstore " & sStore & " " & sSerial)
	Set oStdOut = oExec.StdOut
	Do While oExec.Status = 0
		WScript.Sleep 100
	Loop
	'Uncomment the next four lines to debug certutil:
	'Do Until oStdOut.AtEndOfStream
	'	sLine = oStdOut.ReadLine
	'	wscript.echo sLine
	'Loop
	delCert = oExec.ExitCode
end function

function getCert(sStore)
'Gets the serial numbers of certificates in the machine store specified by "sStore"
'Sends the captured serial numbers to the "delCert" function for deletion.
'Returns: 1 - If cert deletion files, 0 - If no errors are detected.
'Requires: 
'   - WScript.Shell object named "oShell"
'   - Defined path to "certutil.exe" named "sCUPath"
'   - Presence of certutil.exe on the system
	dim oExec, oStdOut
	dim bFail
	dim i, iRet
	dim sLine, sSerial
	
	bFail=False
	Set oExec = oShell.Exec(sCUPath & " -store " & sStore)
	Set oStdOut = oExec.StdOut
	
	Do Until oStdOut.AtEndOfStream
		sLine = oStdOut.ReadLine
		if InStr(1,sLine,"Serial Number",1) then
			i = CInt(InStr(1,sLine,":",1) + 2)
			sSerial = Mid(sLine,i)
			iRet = delCert(sStore,sSerial)
			if (iRet  0) then
				wscript.echo "Certificate deletion failed"
				bFail = True
			end if
		end if
	Loop
	
	if bFail = True then
		getCert = 1
	else
		getCert = 0
	end if
end function

function delFile(sFile)
'Deletes the file specified by "sFile"
'Requires existing Wscript.Shell object named "oShell"
	dim oFSO, oFile
	set oFSO = CreateObject("Scripting.FileSystemObject") 

	'wscript.echo "About to delete file: " & sFile
	if oFSO.FileExists(sFile) then
		'Delete method will force a WSH quit if it fails, so we need to disable exit-on-error:
		Err.Clear
		On Error Resume Next
		
		set oFile = oFSO.GetFile(sFile)
		delFile = oFile.Delete(True)
		if Err.Number = 0 then
			delFile = 0
		else
			delFile = Err.Number
		end if
	else
		'Exit code for the function could be changed here if you are concerned about
		' the requested file to delete not being present on the system.
		'wscript.echo "File " & sFile & " does not exist."
		delFile = 0
	end if
end function

'=-=-=-=-=-=-=-=-=-=-=-=-=
'          MAIN
iRet = stopSvc(SVCNAME, TIMEOUT)
'wscript.echo "Return from stopSvc: " & iRet
if iRet  0 then
	iExit = iExit + 1
end if

iRet = getCert(MACH_STORE)
'wscript.echo "Return from cert deletion for store " & MACH_STORE & ": " & iRet
if iRet  0 then
	iExit = iExit + 2
end if

iRet = getCert(SMS_STORE)
'wscript.echo "Return from cert deletion for store " & SMS_STORE & ": " & iRet
if iRet  0 then
	iExit = iExit + 4
end if

iRet = delFile(sINIPath)
'wscript.echo "Return from file deletion: " & iRet
if iRet  0 then
	iExit = iExit + 8
end if

WScript.Quit iExit

View Desktop Template Building Notes – Nothing is Easy

This week I have been working on updating our VMware View template for our public terminals (those are kiosk and lab systems).  As always, simple things have become difficult, and time has disappeared like cookies at a preschool party. Here are some resources that were useful to me in cleaning up the reference system:

Default User Profile Settings:

Why must this be so hard?  Under XP, you used to be able to tweak the Default User profile to within an inch of its life, then copy it.  Easy!  But MS maintains that this “caused problems” of some vague nature, and so the process is now not possible.  Instead, we are supposed to use the “CopyProfile” action in the unattend.xml file used by sysprep to trigger copying of the “Administrator” profile to “Default”.  However, this operation does not copy all settings… aargh!  And the settings that are excluded are not documented… double aargh!  And if you have any profile traces left over on your system for any user other than “Administrator”, sysprep will fail miserably… triple aargh!

Fortunately, there is help available:
http://blogs.technet.com/b/deploymentguys/archive/2009/10/29/configuring-default-user-settings-full-update-for-windows-7-and-windows-server-2008-r2.aspx

I found especially helpful the following post on managing Windows 7 Taskbar links:
http://sites.uci.edu/itsdeployment/blog/2010/09/01/mdt-pinning-to-taskbar-in-windows-7/

Ultimately, I used a variation on this script to pin items to the Win 7 taskbar:
http://www.msfn.org/board/topic/142521-i-solved-default-user-all-user-start-menu-task-bar-customizat/
(I’ll post the final script I developed separately.  I could not find a fully functional script on the internet.  I had to do some significant mods to the scripts in the above post to get something that does everything that I want.)

Hey, would it not be nice if there were an Group Policy Preference for “shortcuts” to have “taskbar” as a target location?  Yes… yes it would.

Configuration Manager Client Preparation:

I understand you are not supposed to clone a system that has the SCCM client installed on it.  MS documents steps that should be taken to remove the fingerprints of an existing SCCM client prior to cloning… what, so script?

I did some digging and found that the OS Deployment Task Sequences include a step that is supposed to do just this.  After more digging, I determined that the tool for this step is embedded in the SCCM OSD Capture Media.  I generated an OSD Capture Media set, extracted the contents, and found a small executable named “OsdPrepareSmsClient.exe”.  Perhaps this is what I was looking for?  I will test it and find out.

User Profile Cleanup:

Some of our lab admins used to use the profile cleaner utility from MS to erase user profiles from Win2000/XP on logout.  That utility is gone, but you can now use Group Policy to force deletion of all non-Administrator profiles on a schedule.  Unfortunately, this only happens on system restart.

I have settled on “DelProf2.exe” as a replacement:
http://helgeklein.com/free-tools/delprof2-user-profile-deletion-tool/#download
This excellent freeware utility by Helge Klein will delete all profiles that are not in use, and will clean up the ProfileList registry entries as well.  You can exclude specific named profiles as well.

Java Update… make it stop! :

http://www.myitkb.ch/index.php?option=com_k2&view=item&id=207:disable-java-updates-for-all-users&Itemid=60&tmpl=component&print=1

Unofficial Mozilla Builds for Windows:

https://code.google.com/p/htguardmozilla/
(Includes “BlueGriffon” installers that do not embed the obnoxious “iminent toolbar”.  Kudos to Glazman for this excellent basic HTML editor.  Boo to the invasive toolbar installer.)

Unattended Install and Upgrade of Adobe Reader

Previously we explored how to increase the success rate of unattended application upgrades using our handy “killAndExec” VBScript. This works well for about 80% of our applications. What about Adobe Reader? Well… not so much.

Thankfully, the stock Adobe Reader installer deals with open files quite nicely, and does not care if Reader is in use by a browser during silent installs (this is the main reason that we needed killAndExec.vbs in the first place). However, not all Reader install operations are full installs. Interestingly, Adobe is one of the few vendors that I deal with that actually uses MSI patch files (MSP). To install a patch release of Reader (i.e. 10.1.4), you need first to install the base version (10.1), then one or more patch MSP files. This is easy for new installs… just run “setup.exe” silently, then run “msiexec /p (patchFileName) /qn” to install the patch.

However, the situation gets more complicated for upgrades. If the base product already is installed, setup.exe will return an error code. So, for upgrade scenarios, I have put together another handy VBScript to handle base version detection. The script follows:

option explicit
' Install Adobe Reader Script:
' J. Greg Mackinnon, 2012-06-12
' Intended to perform unattended installations of Adobe Reader by MS SCCM 2012.
' 
' Installs the version of Adobe Reader in the same directory as the script, if not already installed.
' Also installs the current Reader patch, if requested.
' Installer string is specified in "sInstall".
' Requires: 
'     Adobe Reader setup and patch files in the same directory as the script.
'     "setup" and "basever" arguments required.
'     "patch" argument optional.
' Returns:
'     - Code 100 - if required arguments are not provided to the script.
'     - Return code of setup program added to the return code of the patch program, if no other errors occur.

dim oExec, oFS, oLog, oShell
dim cScrArgs
dim iExit
dim sBaseVer, sInstall, sLog, sOut, sPatch, sPath, sPF, sScrArg, sTemp, sVer
dim bDoPatch

sLog = "installAdobeReader.log"
iExit = cLng(0)

' Instantiate objects:
Set oShell = CreateObject( "WScript.Shell" )
Set oFS = CreateObject("Scripting.FileSystemObject")
sTemp = oShell.ExpandEnvironmentStrings("%TEMP%")
Set oLog = oFS.OpenTextFile(sTemp & "\" & sLog, 2, True)

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Define Functions
'
function echoAndLog(sText)
'EchoAndLog Function:
' Writes string data provided by "sText" to the console and to Log file
' Requires: 
'     sText - a string containig text to write
'     oLog - a pre-existing Scripting.FileSystemObject.OpenTextFile object
	'If we are in cscript, then echo output to the command line:
	If LCase( Right( WScript.FullName, 12 ) ) = "\cscript.exe" Then
		wscript.echo sText
	end if
	'Write output to log either way:
	oLog.writeLine sText
end function

sub subHelp
	echoAndLog "installAdobeReader.vbs Script"
	echoAndLog "by J. Greg Mackinnon, University of Vermont"
	echoAndLog ""
	echoAndLog "Runs Adobe Reader silent setup (if not already present), then applies "
	echoAndLog "any specified MSP patch files for Reader."
	echoAndLog "Logs output to 'installAdobeReader.log' in the %temp% directory."
	echoAndLog ""
	echoAndLog "Required arguments and syntax:"
	echoAndLog "/setup:""[setupFile]"""
	echoAndLog "     The primary Adobe Reader installation program.  If switches "
	echoAndLog "    are required for setup to run silently, they must be provided."
	echoAndLog "/basever:"
	echoAndLog "     The base Adobe Reader product version for which to check (i.e. 10.1)"
	echoAndLog ""
	echoAndLog "Optional arguments and syntax:"
	echoAndLog "/patch:""[patchFile]"""
	echoAndLog "     MSP patch file to install after Adobe Reader setup completes."
end sub
' End Functions
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Parse Arguments
'
if WScript.Arguments.Named.Count > 0 Then
	Set cScrArgs = WScript.Arguments.Named
'	For Each sScrArg in cScrArgs
'		echoAndLog sScrArg 'Echo supplied arguments to console
'	Next

	for each sScrArg in cScrArgs
		select case LCase(sScrArg)
			Case "setup"
				sInstall = cScrArgs.Item(sScrArg)
			Case "patch"
				sPatch = cScrArgs.Item(sScrArg)
			Case "basever"
				sBaseVer = CStr(cScrArgs.Item(sScrArg))
			Case Else
				echoAndLog vbCrLf & "Unknown switch or argument: " & sScrArg & "."
				echoAndLog "**********************************" & vbCrLf
				subHelp
				oLog.Close
				WScript.Quit(100)
		end select
	next
	if (IsNull(sInstall) or IsEmpty(sInstall)) then
		echoAndLog "Required argument 'setup' was not provided."
		echoAndLog "**********************************" & vbCrLf
		subHelp
		oLog.Close
		wscript.quit(100)
	elseif (IsNull(sBaseVer) or IsEmpty(sBaseVer)) then
		echoAndLog "Required argument 'basever' was not provided."
		echoAndLog "**********************************" & vbCrLf
		subHelp
		oLog.Close
		wscript.quit(100)
	elseif (IsNull(sPatch) or IsEmpty(sPatch)) then 
		bDoPatch = False
	else
		bDoPatch = True
	end if
elseif WScript.Arguments.Named.Count = 0 then 'Detect if required args are not defined.
	echoAndLog vbCrLf & "Required arguments were not specified."
	echoAndLog "**********************************" & vbCrLf
	subHelp
	oLog.Close
	WScript.Quit(100)
end if
'
' End Argument Parsing
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Begin Main
'

' Complete version and installation strings:
sInstall = ".\" & sInstall
sPatch = "msiexec.exe /p " & sPatch & " /qb /norestart"

' Build path to Adobe Reader executable:
sPF = oShell.ExpandEnvironmentStrings( "%ProgramFiles%" )
sPath = sPF & "\Adobe\Reader 10.0\Reader\AcroRd32.exe"
echoAndLog "Acrobat Reader Path: " & sPath

' Get the version string on the currently installed Reader executable:
on error resume next
sVer = oFS.GetFileVersion(sPath)
on error goto 0
echoAndLog "Version of currently installer Adober Reader: " & sVer

' See if we already have the base version installed:
if InStr(Left(cStr(sVer),4),sBaseVer) then
	'Reader 10.1.x is already installed skip base product installation.
	echoAndLog "Base product installed.  Skipping setup..."
else
	'Install the base product.
	echoAndLog "Installing base product with command: " & sInstall
	set oExec = oShell.Exec(sInstall)
	Do While Not oExec.Status  1
		WScript.Sleep(100)
	Loop 
	sOut = oExec.StdOut.ReadAll()
	echoAndLog "Return code from installer: " & oExec.ExitCode
	echoAndLog "Standard output: " & sOut
	iExit = cLng(oExec.ExitCode)
end if

'Now install the patch:
if bDoPatch = True then
	echoAndLog "Patch installation requested."
	echoAndLog "Installing patch with command: " & sPatch
	set oExec = oShell.Exec(sPatch)
	Do While Not oExec.Status  1
		WScript.Sleep(100)
	Loop 
	sOut = oExec.StdOut.ReadAll()
	echoAndLog "Return code from patch installer: " & oExec.ExitCode
	echoAndLog "Standard output: " & sOut
	iExit = cLng(oExec.ExitCode) + iExit
else 
	echoAndLog "Patch installation was not requested.  Exiting."
end if

oLog.Close
wscript.quit(iExit)
'
' End Main
'''''''''''''''''''''''''''''''''''''''''''''''''''

KillAndExec.vbs – Ensuring application installer success with VBScript

Today’s scripting challenge…

We are attempting to use SCCM 2012 as a patch management solution for our centrally supported third party applications.  Great new features in SCCM 2012 allow us to write detection rules for applications to determine if superseded versions are present on the client system, and to trigger an immediate upgrade.  Cool Beans.  Problem is, a lot of application installers that ran reliably in our MDT “LiteTouch” environment (which is used to deploy new operating systems with no previously installed software) will not run silently or successfully on systems where previous application versions were already installed, and may currently be running.

This is an old problem for client system management… how can you update in-use files?  In most cases I have seen, the admin will schedule the updates to run when no one is logged in.  Unfortunately, this is an edge case for us.  Most systems are off when no one is logged in.  Another system is to force logoff for application updates.  While this would work, it seems like a “heavy” solution… why force the user to log off to update one application that may or may not be running?  Why force all applications closed on the off chance that one application will need to be terminated.

Our solution?  Kill only the processes that need to be terminated to ensure application installation success.  See the VBScript solution below (I flirted with writing this one in PowerShell, but the code signing requirements still intimidate me, and I may have the odd-duck XP client that still does not have PowerShell).  I have tested the script on Firefox, Thunderbird, VLC, Notepad++, WinSCP, Filezilla, and KeePass.  Rock On!

UPDATE: Since initial publication, I have added some logic to handle execution from “wscript”. If the script is executed from wscript.exe, console output will be suppressed. Additionally, the log file now is named “killAndExec-(exeFileName).log”. (This prevents SCCM from overwriting the log file the next time a program installer runs that also uses this script).

'KillAndExec.vbs script, J. Greg Mackinnon, 2012-09-13
' Kills processes named in the "kill" argument (comma-delimited)
' Runs the executable named in the "exec" argument
' Appends the executable arguments specified in the "args" argument (comma-delimited)
'Requires: "kill" and "exec" arguments.  The executable named in the "exec" arg must be in the same directory as this script.
'Provides:
' RC=101 - Error terminating the requests processes
' RC=100 - Invalid input parameters
' Other return codes - Pass-though of return code from WShell.Exec.Run using the provided input parameters

Option Explicit

const quote = """"

'Declare Variables:
Dim aExeArgs, aKills
Dim bBadArg, bNoArgs, bNoExeArg, bNoExec, bNoKill, bNoKillArg 
Dim cScrArgs
Dim iReturn
Dim oShell, oFS, oLog
Dim sBadArg, sCmd, sExe, sExeArg, sKill, sLog, sScrArg, sTemp

'Set initial values:
bBadArg = false
bNoArgs = false
bNoExeArg = false
bNoExec = false
bNoKill = false
bNoKillArg = false
iReturn = 0

'Instantiate Global Objects:
Set oShell = CreateObject("WScript.Shell")
Set oFS  = CreateObject("Scripting.FileSystemObject")

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Define Functions
'
Sub subHelp
	echoAndLog "KillAndExec.vbs Script"
	echoAndLog "by J. Greg Mackinnon, University of Vermont"
	echoAndLog ""
	echoAndLog "Kills named processes and runs the provided executable."
	echoAndLog "Logs output to 'KillAndExec.vbs' in the %temp% directory."
	echoAndLog ""
	echoAndLog "Required arguments and syntax:"
	echoAndLog "/kill:""[process1];[process2]..."""
	echoAndLog "     Specify the image name of one or more processes to terminate."
	echoAndLog "/exe:""[ExecutableFile.exe]"""
	echoAndLog "     Specify the name of the executable to run."
	echoAndLog ""
	echoAndLog "Optional arguments:"
	echoAndLog "/args""[arg1];[arg2];[arg3]..."""
	echoAndLog "     Specify one or more arguments to pass to the executable."
	echoAndLog "/noKill"
	echoAndLog "     Switch to suppress default process termination.  Used for testing."
	echoAndLog "/noExec"
	echoAndLog "     Switch to suppress default program execution.  USed for testing."
End Sub

function echoAndLog(sText)
'EchoAndLog Function:
' Writes string data provided by "sText" to the console and to Log file
' Requires: 
'     sText - a string containig text to write
'     oLog - a pre-existing Scripting.FileSystemObject.OpenTextFile object
	'If we are in cscript, then echo output to the command line:
	If LCase( Right( WScript.FullName, 12 ) ) = "\cscript.exe" Then
		wscript.echo sText
	end if
	'Write output to log either way:
	oLog.writeLine sText
end function

function fKillProcs(aKills)
' Requires:
'     aKills - an array of strings, with each entry being the name of a running process.   
	Dim cProcs
	Dim sProc, sQuery
	Dim oWMISvc, oProc

	Set oWMISvc = GetObject("winmgmts:{impersonationLevel=impersonate, (Debug)}\\.\root\cimv2")
	sQuery = "Select Name from Win32_Process Where " 'Root query, will be expanded.	
	'Complete the query string using process names in "aKill"
	for each sProc in aKills
		sQuery = sQuery & "Name = '" & sProc & "' OR "
	next
	'Remove the trailing " OR" from the query string
	sQuery = Left(sQuery,Len(sQuery)-3)

	'Create a collection of processes named in the constructed WQL query
	Set cProcs = oWMISvc.ExecQuery(sQuery, "WQL", 48)
	echoAndLog vbCrLf & "----------------------------------"
	echoAndLog "Checking for processes to terminate..."
	'Set this to look for errors that aren't fatal when killing processes.
	On Error Resume Next
	'Cycle through found problematic processes and kill them.
	For Each oProc in cProcs
	   echoAndLog "Found process " & oProc.Name & "."
	   oProc.Terminate()
	   Select Case Err.Number
		   Case 0
			   echoAndLog "Killed process " & oProc.Name & "."
			   Err.Clear
		   Case -2147217406
			   echoAndLog "Process " & oProc.Name & " already closed."
			   Err.Clear
		   Case Else
			   echoAndLog "Could not kill process " & oProc.Name & "! Aborting Script!"
			   echoAndLog "Error Number: " & Err.Number
			   echoAndLog "Error Description: " & Err.Description
			   echoAndLog "Finished process termination function with error."
			   echoAndLog "----------------------------------"
			   echoAndLog vbCrLf & "Kill and Exec script finished."
			   echoAndLog "**********************************" & vbCrLf
			   WScript.Quit(101)
	   End Select
	Next
	'Resume normal error handling.
	On Error Goto 0
	echoAndLog "Finished process termination function."
	echoAndLog "----------------------------------"
end function

function fGetHlpMsg(sReturn)
' Gets known help message content for the return code provided in "sReturn".
' Requires:
'     Existing WScript.Shell object named "oShell"
	Dim sCmd, sLine, sOut
	Dim oExec
	sCmd = "net.exe helpmsg " & sReturn
	echoAndLog "Help Text for Return Code:"
	set oExec = oShell.Exec(sCmd)
	Do While oExec.StdOut.AtEndOfStream  True
		sLine = oExec.StdOut.ReadLine
		sOut = sOut & sLine
	Loop
	fGetHlpMsg = sOut
end function
'
' End Define Functions
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Parse Arguments
If WScript.Arguments.Named.Count > 0 Then
	Set cScrArgs = WScript.Arguments.Named
	For Each sScrArg in cScrArgs
		Select Case LCase(sScrArg)
			Case "nokill"
				bNoKill = true
			Case "noexec"
				bNoExec = true
			Case "kill"
				aKills = Split(cScrArgs.Item(sScrArg), ";", -1, 1)
			Case "exe"
				sExe = cScrArgs.Item(sScrArg)
			Case "args"
				aExeArgs = Split(cScrArgs.Item(sScrArg), ";", -1 ,1)
			Case Else
				bBadArg = True
				sBadArg = sScrArg
		End Select
	Next
	If (IsNull(sExe) or IsEmpty(sExe)) Then
		bNoExeArg = True
	ElseIf (IsNull(aKills) or IsEmpty(aKills)) Then
		bNoKillArg = True
	End If
ElseIf WScript.Arguments.Named.Count = 0 Then 'Detect if required args are not defined.
	bNoArgs = True
End If 
' End Argument Parsing
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Initialize Logging
sTemp = oShell.ExpandEnvironmentStrings("%TEMP%")
sLog = "killAndExec-" & sExe & ".log"
Set oLog = oFS.OpenTextFile(sTemp & "\" & sLog, 2, True)
' End Initialize Logging
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Process Arguments
if bBadArg then
	echoAndLog vbCrLf & "Unknown switch or argument: " & sBadArg & "."
	echoAndLog "**********************************" & vbCrLf
	subHelp
	WScript.Quit(100)
elseif bNoArgs then
	echoAndLog vbCrLf & "Required arguments were not specified."
	echoAndLog "**********************************" & vbCrLf
	subHelp
	WScript.Quit(100)
elseif bNoExeArg then
	echoAndLog "Required argument 'exe' was not provided."
	echoAndLog "**********************************" & vbCrLf
	subHelp
	wscript.quit(100)
elseif bNoKillArg then
	echoAndLog "Required argument 'kill' was not provided."
	echoAndLog "**********************************" & vbCrLf
	subHelp
	wscript.quit(100)
end if
' Log processes to kill:
for each sKill in aKills
	echoAndLog "Process to kill: " & sKill
next
' Log executable arguments:
echoAndLog "Executable to run: " & sExe
if not (IsNull(aExeArgs) or IsEmpty(aExeArgs)) then
	for each sExeArg in aExeArgs
		echoAndLog "Executable argument: " & sExeArg
	next
else 
	echoAndLog "Executable has no provided arguments."	
end if
' End Process Arguments
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
'Begin Main
'
'Build full command string:
if inStr(sExe," ") then 'Spaces in the exe file
	sExe = quote & sExe & quote 'Add quotations around the executable.
end if
if not (IsNull(aExeArgs) or IsEmpty(aExeArgs)) then
	sCmd = sExe & " " 
	for each sExeArg in aExeArgs
		if inStr(sExeArg," ") then
			sExeArg = quote & sExeArg & quote 'Add quotations around the argument.
		end if
		sCmd = sCmd & sExeArg & " "
	next
else
	sCmd = sExe
end if
echoAndLog "Command to execute:"
echoAndLog sCmd

'Kill requested processes:
if bNoKill = false then
	fKillProcs aKills
else
	echoAndLog "/noKill switch has been set.  Processes will not be terminated."
end if
'Run the requested command:
echoAndLog vbCrLf & "----------------------------------"
if bNoExec = false then
	echoAndLog "Running the command..."
	on error resume next 'Disable exit on error to allow capture of oShell.Run execution problems.
	iReturn = oShell.Run(sCmd,10,True)
	if err.number  0 then 'Gather error data if oShell.Run failed.
	    echoAndLog "Error: " & Err.Number
		echoAndLog "Error (Hex): " & Hex(Err.Number)
		echoAndLog "Source: " &  Err.Source
		echoAndLog "Description: " &  Err.Description
		iReturn = Err.Number
		Err.Clear
		wscript.quit(iReturn)
	end if
	on error goto 0
	echoAndLog "Return code from the command: " & iReturn
	if iReturn  0 then 'If the command returned a non-zero code, then get help for the code:
		fGetHlpMsg iReturn
	end if 
else
	echoAndLog "/noExec switch has been set.  Executable will not run."
end if
echoAndLog "----------------------------------"

oLog.Close
wscript.quit(iReturn)
'
' End Main
'''''''''''''''''''''''''''''''''''''''''''''''''''

Driver installation with SCCM Software Distribution

Here we are, working with SCCM again.  Making difficult things possible, and simple things difficult.  Today we wish to distribute a SmartCard driver to all of our managed servers, so that we can require Smart Card for certain classes of logins.  the newer “CNG” Smart Card minidrivers are all simple “.inf” driver packages that you can right-click install.  This ought to be easy, thought the sys admin.  Wrong!

Installation of inf drivers is not a well documented command line procedure (unlike the rather more complicated “.msi” package, which at least is easy to script).

My thanks goes out to the following bloggers and forum users for their assistance with this case:

The script that I cobbled together to install the Athena “ASECard” minidriver is displayed below.  Note that this should work for pretty much any minidriver, as long as it has a “DefaultInstall” section in the inf file.  I just unpack the amd64 and x86 driver cab files into their respective directories, put the batch script one directory above these, and make an SCCM software package of the whole thing.  The installation command line is simply the batch file name.

@echo off
REM Installs the drivers specified in the "DefaultInstall" section
REM of the aseMD.inf that is appropriate for the current (x86 or amd64) platform.
REM Install is silent (4 flag), with no reboot (N flag).
REM The INF is specified to be in the x86 or amd64 subdirectory
REM of the script directory (%~dp0).

echo Detecting platform…
IF EXIST "%programfiles(x86)%" (GOTO :amd64) ELSE (GOTO :i386)

:i386
echo Installing 32-bit driver…
cd x86
%windir%\system32\rundll32.exe advpack.dll,LaunchINFSectionEx "%~dp0x86\aseMD.inf",DefaultInstall,,4,N
goto :EOF

:amd64
REM The command will run in 64-bit mode (%windir%\sysnative\),
REM when called from a 32-bit CMD.exe (as will be the case with SCCM).
echo Installing 64-bit driver…
cd amd64
%windir%\sysnative\rundll32.exe advpack.dll,LaunchINFSectionEx "%~dp0amd64\aseMD.inf",DefaultInstall,,4,N
goto :EOF
REM End of file