Category Archives: Scripting the Windows Experience

Some Windows scripting work that I have done to help get my job done.

Server 2012 boot hang in vSphere/ESXi

Over the past year or so we have been having some problems with Server 2012 and 2012 R2 virtual machines hanging during reboot operations. The systems hang at the “spash screen”, showing the Windows logo and the ring of spinning dots… forever!

Finally I was able to find an fix for this problem here:
http://kb.vmware.com/kb/2092807

From a tip-off here:
https://social.technet.microsoft.com/Forums/windowsserver/en-US/595c3048-4d70-48ad-a78e-9380df1bbd70/windows-2012-r2-sometimes-hangs-at-splash-screen-after-reboot?forum=winserver8gen

The problem? Well, probably it is best that you just read the TechNet social thread, if you really want to know. It is none too exciting, and all very aggravating. The fix? Run a PowerShell script, then vMotion your machines to force ESXi to re-read the VMX file for your guests.

I am posting my variation on the script in the KB here, because VMware’s script is incomplete, and difficult to read.

#Source: http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=2092807
#Script intended to correct the bug where Windows Server 2012+ systems on VM Hardware v10 will hang at the 
#boot-up splash screen.  Problem is caused by the failure of ESX to clear the "TS" counter on system reboot.

Set-PSDebug -Strict

#Initialize the VIToolkit:
if ( (Get-PSSnapin -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue) -eq $null ) {
    Add-PsSnapin VMware.VimAutomation.Core
}
[Reflection.Assembly]::LoadWithPartialName("VMware.Vim")

#Connect to your virtual center:
$viServ = "myVCenter.domain.com"
Connect-VIServer -Server $viServ

#Get all VMs in the vCenter:
$vms = Get-VM 
#Loop though the VMs:
ForEach ($vm in $vms){
	#Get a "View" object for each VM.  Views expose useful data that is not contined in the VM object:
	$vmv = Get-VM $vm | Get-View
	$name = $vmv.Name
	$guestid = $vmv.Summary.Config.GuestId
	
	if ($guestid -like "windows8*Guest") {
		#windows8*Guest will match Windows 8 client 32-bit and 64-bit, as well as 
		# "windows8Server64Guest" (which is Windows Server 2012 and 2012 R2).
		
		#We need to update the VMX file for the VM, which is dong using a VirtualMachineConfigSpec.
		$vmx = New-Object VMware.Vim.VirtualMachineConfigSpec
		$vmx.extraConfig += New-Object VMware.Vim.OptionValue
		$vmx.extraConfig[0].key = "monitor_control.enable_softResetClearTSC"
		$vmx.extraConfig[0].value = "TRUE"
		($vmv).ReconfigVM_Task($vmx)
		write-host "Edited" $vmv.name  
		$vmv.name | out-file -FilePath c:\local\temp\softTscOut.txt -NoClobber -Append
	}
}

Discover user rights in SharePoint

Among the top items capable of derailing your whole day or week are requests from auditors.  Who has access to a resource?  When did they exercise those rights?  In the pas few months, I have had several requests of this sort related to SharePoint rights.  Since I have once again started working on our SharePoint 2010-to-2013 migration project, and most of the SharePoint Powershell cmdlets were fresh in my mind, I though I would take a crack at this somewhat intimidating task.

As usual, writing a useful script took more time that I would have liked, but I am fairly pleased with the results.  The final product makes heavy use of Regular Expressions.  Special thanks go out to RegEx Hero, an online .NET regular expressions tester:
http://regexhero.net/tester/
AND, of course, to the Regular-Expressions.info site:
http://www.regular-expressions.info/

Using .NET-style RegEx named capture groups, I was able to eliminate redundant loops though the SharePoint web site list, thus making it possible to crawl all SharePoint web and site-level ACLs in only a few minutes. Hurray!

This code will work only on SharePoint 2010 farms that use Windows authentication. There may be limitations related to sites with multiple Windows domains as well. I will need to update this script in the near future to handle claims authentication, but we will cross that bridge when we come to it.

The script has some pretty convoluted loops that may not make any intuitive sense… I have tried to insert comments to explain what is going on. If you do choose to use this script in your environment and find it difficult to understand, feel free to contact me with questions.

I think that the script has been posted without wordpress-induced errors. The main problem there is the use of .NET style named capture groups, which use ‘greater than’/’less than’ characters. WordPress hates gt/lt, which resemble XML/HTML/XHTML tags. I had to enter these bits of code as HTML escape sequences instead of raw code. It looks like it worked this time.

#findUserPermissions.ps1
#J. Greg Mackinnon, 2015-03-25
#
#Recurses though the web application provided in -webAppplication for the users (samaccountname format) provided in -Users,
#for the domain provided in -Domain.
#
# Requires: Microsoft.SharePoint.PowerShell PSSnapin
#          ActiveDirectory PowerShell module
#
# Provides: Comma-separated value file with user, permission, and site data for all discovered permissions, 
#          at path provided in the -logPath parameter.          
[cmdletBinding()]
param(
    [parameter(
        Mandatory=$true,
        HelpMessage='Enter a username or comma-separated list of usernames in samAccountName format.')]
        [ValidatePattern('^\b[\w\.-]{1,20}\b$')]
        [string[]]$users,
    [parameter()]
        [ValidatePattern('\b[\w\.-]{1,15}\b')]
        [string]$domain = 'CAMPUS',
    [parameter(
        HelpMessage='Enter the URL of the SharePoint web application for which all webs will be searched.')]
        [ValidatePattern('^http[s]*://\w+\.\w+')]
        [string]$webApplication,
    [parameter(
        HelpMessage='Enter the URL of the single SharePoint site for which all subwebs will be searched.')]
        [ValidatePattern('^http[s]*://\w+\.\w+')]
        [string]$spSite,
    [string]$logPath = 'c:\local\temp\findUserPermissions.log'
)
Set-PSDebug -Strict

function getPermType($mask) {
    #SharePoint permissions/roles are exposed programatically as "PermissionMask" attribute of the SPWeb.Permissions.Member objects.
    #Most of these masks are numeric.  This function will convert the numeric value into a "friendly" string, derived by examining 
    #site permissions in a web browser:
    [string]$return = switch ($mask) {
        'FullMask'   {[string]'FullControl'}
        '1012866047' {[string]'Design'}
        '1011028719' {[string]'Contribute'}
        '138612833'  {[string]'Read'}
        '138612801'  {[string]'ViewOnly'}
        '134287360'  {[string]'List-Library:UnknownAccess'} #Note that this mask has the label "Limited Access" in the GUI.
        default      {[string]"Unknown:$mask"}
    }
    return $return
}

function checkADGroupMembers {
    param ([string]$adGroupName,[string]$adGroupSid,[string]$userRegex)
    #Searches the AD Group provided in -adGroup for matches against the regex provided in -userRegex.
    #Search is performed against the domain of the computer running the script.
    #The userRegex value can be generated using the "regexifyDomainUser" function. It MUST capture the 
    # username/samAccountName to a named capture group called 'name'.
    #Returns boolean true/false.
    #Need to add ability to return RegEx match objects from the search.
    #Optional ability to specify the domain to search?
    
    [Int32]$i = $adGroupName.IndexOf('\')
    [string]$groupSam = $adGroupName.Substring($i + 1)
    #write-host "Getting members of:" $groupSam
    
    [String[]]$returns = @()
    
    #Domain Users could take a long time to process, so let's just assume that the user is a domain user:
    if (($groupSam -eq 'domain users') -or ($groupSam -eq 'authenticated users')) {
        $returns += "!AllUsers!"
        return $returns
        break
    }
    #Get-ADGroupMember will error frequently because SharePoint contains a lot of orphaned groups.
    #Use "ErrorAction Stop" for force an breaking error when this happens, and just set $match to $false/
    try {
        [array]$grpMembers = @()
        if ($adGroupSid) {
            $grpMembers += Get-ADGroupMember -Recursive -Identity $adGroupSid -ErrorAction Stop `
                | Select-Object -ExpandProperty SamAccountName 
        } else {
            $grpMembers += Get-ADGroupMember -Recursive -Identity $groupSam -ErrorAction Stop `
                | Select-Object -ExpandProperty SamAccountName 
        }
    } catch {
        write-host "    AD group $groupSam does not exist" -ForegroundColor Red
        $returns = $null
        break
    }
    if ($grpMembers.count -gt 0) {
        foreach ($memb in $grpMembers) {
            #write-host "testing" $memb "against" $userregex
            if ($memb -match $userRegex) {
                #write-host "regexmatch found: " $matches.user
                #Return the current regex named group "user".  THis is just showing off...
                #I could simply skip the capture groups and just return $memb.
                $returns += $matches.user
            }
        }
    } else {
        write-host "    AD group $groupSam has no members" -ForegroundColor Red
    }
    if ($returns.count -gt 0) {
        return $returns
    } else {
        return $null
    }
}

function regexifyDomainUser {
    param ([string]$user,[string]$domain)
    #Converts the provided domain\username pair into a regex that can 
    #be used to search for the same pattern in a larger string.
    #This regex will return the username in a capture group named "user".
    if ($user -match '\.') {
        $userMatch = $user.Replace('.','\.')
    } else {
        $userMatch = $user
    }
    [string]$regexUser = '^' + $domain + '\\(?<user>' + $userMatch + ')$'
    return $regexUser
}
function regexifyUser {
    #Converts the provided username into a regex that can 
    #be used to search for the same pattern in a larger string.
    #This regex will return the username in a capture group named "user".
    param ([string]$user)
    if ($user -match '\.') {
        $userMatch = $user.Replace('.','\.')
    } else {
        $userMatch = $user
    }
    [string]$regexUser = '^(?<user>' + $userMatch + ')$'
    return $regexUser
}

if ((Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null) {
    Add-PSSnapin Microsoft.SharePoint.PowerShell
}
Import-Module ActiveDirectory

#Set up regex strings that contain all of the samAccountNames provided in the input parameters.
#We want to be able to match all possible users with one regex for processing efficiency.

#DomainUsers will match against DOMAIN\username.
[string]$regexDomainUsers = regexifyDomainUser -user $users[0] -domain $domain
#Users will match againstjust username.
[string]$regexUsers = regexifyUser -user $users[0]
if ($users.count -gt 1) {
    for ($i=1; $i -lt $users.count; $i++) {
        #Set up a regex for matching the user in domain\username format:
        [string]$regexDomainUser = '|' + $(regexifyDomainUser -user $users[$i] -domain $domain)
        $regexDomainUsers += $regexDomainUser
        [string]$regexUser = '|' + $(regexifyUser -user $users[$i])
        $regexUsers += $regexUser
    }
}

## Initialize output log:
if (Test-Path -LiteralPath $logPath) {
    Remove-Item -LiteralPath $logPath -Force
}
#CSV header row:
[string]$out = "user,Role,WebUrl,aceDetails"
$out | Out-File -FilePath $logPath 

#Get selected SharePoint sites:
[array]$sites = @()
if ($spSite) {                 #If spSite is specified, search only one site:
    $sites += Get-SPSite -Identity $spSite -Limit All
} elseif ($webApplication){    #If webApplication is specified, search all sites in the webapp:
    $sites += Get-SPSite -WebApplication $webApplication -Limit All
} else {                       #Otherwise, search all web applications defined on the local farm:
    $sites += Get-SPSite -Limit All
}

##Begin Main Loop:
foreach ($site in $sites) {
    $webs = @()
    # Gets webs in current site
    $webs = $site | % {Get-SPWeb -Site $_ -Limit All}
    foreach ($web in $webs) {
        Write-Host "Testing web:" $web.url -ForegroundColor cyan 
        $webPerms = @()
        $webPerms = $web.Permissions
        foreach ($perm in $webPerms) {
            #Scenario 1: ACL entry is for this specific user:
            if ($perm.member.loginName -match $regexDomainUsers) {
                [string]$user = ($perm.member.loginName).split('\') | select -Last 1
                write-host "    Found match $user in the web ACL list." -ForegroundColor yellow
                [string]$aclData = 'Acl:Direct'
                [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData
                $out | Out-File -Append -FilePath $logPath
            }
            #Scenario 2: ACL entry is an Active Directory Group
            if ($perm.Member.IsDomainGroup) {
                [String[]]$ADGroupusers = @()
                $ADGroupUsers = checkADGroupMembers -adGroupName $perm.Member.LoginName -adGroupSid $perm.Member.Sid -userRegex $regexUsers
                if ($ADGroupUsers.count -gt 0) {
                    foreach ($user in $ADGroupUsers) {
                        write-host "    Found user $user in AD Group:" $perm.member.loginName ", which is on the web ACL list." -ForegroundColor yellow
                        [string]$aclData = 'Acl:Embedded:' + $perm.Member.LoginName 
                        [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData 
                        $out | Out-File -Append -FilePath $logPath
                    }
                }
            }
            #Scenarios: ACE is for a SharePoint group:
            if ($perm.Member.GetType().Name -eq 'SPGroup') {
                $members = @()
                $members += $perm.member.users
                foreach ($member in $members) {
                    #Scenario 3: ACL is a SharePoint group, and the group contains a matching user:
                    if ($member.loginName -match $regexDomainUsers) {
                        [string]$user = ($member.loginName).split('\') | select -Last 1
                        write-host "    Found match for" $user "in a Sharepoint group that is in the web site ACL." -ForegroundColor yellow

                        [string]$aclData = 'SPGroup:Direct:' + $perm.Member.LoginName
                        [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData
                        $out | Out-File -Append -FilePath $logPath
                    } #End Scenario 3
                    #Scenario 4: ACL is a SharePoint group, and the group contains an AD group that contains a matching user:
                    if ($Member.IsDomainGroup) {
                        [String[]]$ADGroupUsers = @()
                        $ADGroupUsers = checkADGroupMembers -adGroupName $Member.LoginName -adGroupSod $Member.LoginName.Sid -userRegex $regexUsers
                        if ($ADGroupUsers.count -gt 0) {
                            foreach ($user in $ADGroupUsers) {
                                write-host "    Found user $user in an AD Group that is found in a SharePoint group that appears in the web site ACL." -ForegroundColor yellow
                                [string]$aclData = 'SPGroup:Embedded:' + $perm.Member.LoginName + ':' + $member.loginName
                                [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData
                                $out | Out-File -Append -FilePath $logPath
                            }
                        }
                    } #End Scenario 4
                }
            } #End SPGroup Eval
        } #End foreach $perm
        foreach ($admin in $web.SiteAdministrators) {
            #Enumerate Web Site Administrators (should be at least two)
            if ($admin.LoginName -match $regexDomainUsers) {
                [string]$user = $matches.user
                write-host "    Found web site administrator match for:" $user -ForegroundColor yellow
                [string]$out = $user + ',' + 'webSiteAdministrator,' + $web.Url
                $out | Out-File -Append -FilePath $logPath
                #exit #for debugging
            }
        }
        $web.Dispose()
    }
    if ($site.owner.UserLogin -match $regexDomainUsers) {
        #Discover Site Owner (should be only one)
        [string]$user = $matches.user
        write-host "    Found site collection owned by:" $user -ForegroundColor yellow
        [string]$out = $user + ',' + 'SiteCollectionOwner,' + $site.Url
        $out | Out-File -Append -FilePath $logPath
        #exit #for debugging
    }
    $site.Dispose()
}

Migrating to the SCCM UDI for OSD, part 4: Applications

Continued from part 3b:
http://blog.uvm.edu/jgm/2015/03/10/sccm-udi-3b-os/

Previously in this series I demonstrated how you can manage Operating System selection and application in UDI, and one (rather complicated) method for managing drivers in SCCM/UDI. To complete our UDI experience, I now present a handy script for automation of populating select SCCM applications into UDI.

There is some conventional wisdom floating around out there in the ‘tubes that you should not deploy the new SCCM 2012-style “Applications” in SCCM OSD Task Sequences. Instead, it is asserted that you should use only traditional application “Packages”. Since we have developed all or our application installer using the new “Application” objects, I have chosen to ignore this guidance. Fortunately, it appears that the earlier bugs that led to dispensing of this advice have been corrected in the 2012 R2 CU2-CU3 timeframe.

The script below will make use of Configuration Manager application tags to determine which Applications should be populated into UDI. Additionally, the tags are used to generate application groups in the UDI wizard. If you don’t want an application to appear in UDI, don’t tag it. Applications can have multiple tags, and thus can end up getting defined more than once un UDI. This does not appear to cause any problems during deployment.

This script will re-write the entire UDI designer “.app” file, which is an XML-formatted document that defines all of the applications and groups that will be displayed during the UDI Wizard (the default name for this file is ‘UDIWizard_Config.xml.app’). Because I am writing out the entire file, I chose to use the System.Xml.XmlTextWriter .Net Framework class to do the heavy lifting for me. I probably could have used System.Xml.XmlDocument (which I used in part 3 of this series for updating the main UDI XML control file), but the XmlTextWriter seemed to be a more direct route to getting the job done in this case.

Note that in this code I am again using the SCCM PowerShell cmdlets. In this case, I am using Get-CMCategory and Get-CMApplication. These commands easily could be replaced with simple WMI queries (as seen in part 3 of this series). I also am using some SCCM managed code. I call the “Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer” class in order to use the “Deserialize” method. This method converts the large volume of data in the SDMPackageXML attribute of the application object into somewhat more accessible PowerShell objects.

To use this script in your environment, you will need to update the $outPath, $CMSiteCode, and $CMBinPath variables to match your environment. After running the script, you will need to redistribute the content of your MDT Files package to your distribution points. You UDI clients will read the new data as soon as it is made available on your DP.

# build-UDIImageList.ps1
# J. Greg Mackinnon
# Created: 2015-01-26
# Updated: 2015-03-17 - Added host output to indicate task progress.  Removed block comments for blogging clarity.
# Populates the UDI Configuration Wizard XML APP file with all categorized applications gathered 
# from Configuration Manager.  
# Requires: a local installation of the Configuration Manager administration tools.
#   Modify $outPath, $CMSiteCode, and $CMBinPath to match your environment.
Set-PSDebug -Strict

[string] $outPath = 'O:\sources\os\mdt\files\Scripts\UDIWizard_Config.xml.app'

[string] $CMSiteCode = 'UVM'
[string] $CMBinPath = 'F:\Program Files\Microsoft Configuration Manager\AdminConsole\bin\'
[string] $CMModName = 'ConfigurationManager.psd1'
[string] $CMModPath = Join-Path -Path $CMBinPath -ChildPath $CMModName

[string] $CMDrive = $CMSiteCode + ':\'

Import-Module -Name $CMModPath | out-null
Push-Location $CMDrive

#Gather current "Administrative Categories" used to classify Applications, capture to an Array of strings:
write-host "Gathering Application Categories..."
[String[]] $CMAppGroups = Get-CMCategory -CategoryType AppCategories | % {$_.LocalizedCategoryInstanceName}

[int] $appCount = 1

# Output XML requires: 
    # DisplayName (map to LocalizedDisplayName)
    # Name, 
    # Guid (with ScopeID, ApplicationGUID... maps to "ModelName" CMApplication Property), 
    # description (optional? map to LocalizedDescription), 
    # type (deployment type?), 
    # ProductID (which can be found in the sdmpackagexml.deploymenttypes[#].installer.productcode)
# All separated into "ApplicationGroup" stanzas, with name= attributes, I think we can map this to "LocalizedCategoryInstanceNames"


#$xmlDoc = New-Object System.Xml.XmlDocument # Note this is the same as the 1 type accelerator
$utf8 = New-Object System.Text.UTF8Encoding
# Create The Document Writer:
$XmlWriter = New-Object System.XMl.XmlTextWriter($outPath,$utf8)
$xmlWriter.Formatting = "Indented"
$xmlWriter.Indentation = "5"
$xmlWriter.WriteStartDocument()
$xmlWriter.WriteStartElement("Applications")
$xmlWriter.WriteAttributeString('RootDisplayName','Applications')

write-host "Gathering CM Applications..."
$CMApps = Get-CMApplication | Select-Object -Property LocalizedDisplayName,LocalizedDescription,ModelName,LocalizedCategoryInstanceNames,IsLatest,IsExpired,IsSuperseded,SDMPackageXML | ? {$_.IsLatest -and ($_.IsExpired -eq $false) -and ($_.IsSuperseded -eq $false)}

write-host
foreach ($group in $CMAppGroups) {
    $xmlWriter.WriteStartElement('ApplicationGroup')
    $xmlWriter.WriteAttributeString('Name',$group)
    write-host "Processing group: $group" -ForegroundColor yellow
    
    foreach ($app in $CMApps) {
        if ($app.LocalizedCategoryInstanceNames.contains($group)) {
            write-host "    Adding application " $app.LocalizedDisplayName -ForegroundColor cyan
            $xmlWriter.WriteStartElement('Application')
            $xmlWriter.WriteAttributeString('DisplayName',$app.LocalizedDisplayName)
            $xmlWriter.WriteAttributeString('State','enabled')
            $xmlWriter.WriteAttributeString('Id',[string]$appCount)
            $xmlWriter.WriteAttributeString('Name',$app.LocalizedDisplayName)
            $xmlWriter.WriteAttributeString('Guid',$app.ModelName)

            $appXml = [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::Deserialize($app.SDMPackageXML,$true)

                $xmlWriter.WriteStartElement('Setter')
                $xmlWriter.WriteAttributeString('Property','description')
                $xmlWriter.WriteEndElement()

                $xmlWriter.WriteStartElement('Dependencies')
                $xmlWriter.WriteEndElement()

                $xmlWriter.WriteStartElement('Filters')
                $xmlWriter.WriteEndElement()

                $xmlWriter.WriteStartElement('ApplicationMappings')
                
                    $xmlWriter.WriteStartElement('Match')           
                    $xmlWriter.WriteAttributeString('Type','WMI')
                    $xmlWriter.WriteAttributeString('OperatorCondition','OR')
                    $xmlWriter.WriteAttributeString('DisplayName',$app.LocalizedDisplayName)
                        $xmlWriter.WriteStartElement('Setter')
                        $xmlWriter.WriteAttributeString('Property','Name')
                            $xmlWriter.WriteString($app.LocalizedDisplayName)
                        $xmlWriter.WriteEndElement() # <-- End Setter
                    $xmlWriter.WriteEndElement() # <-- End Match
                    foreach ($type in $appXml.DeploymentTypes) {
                        $xmlWriter.WriteStartElement('Match')
                        $xmlWriter.WriteAttributeString('Type','MSI')
                        $xmlWriter.WriteAttributeString('OperatorCondition','OR')
                        $xmlWriter.WriteAttributeString('DisplayName',$app.LocalizedDisplayName)
                            $xmlWriter.WriteStartElement('Setter')
                            $xmlWriter.WriteAttributeString('Property','ProductId')
                                if ($type.Installer.Technology -match 'MSI') {
                                    $xmlWriter.WriteString($type.Installer.ProductCode)
                                } else {
                                    $xmlWriter.WriteString(' ')
                                }
                            $xmlWriter.WriteEndElement() # <-- End Setter
                        $xmlWriter.WriteEndElement() # <-- End Match
                    }
                $xmlWriter.writeEndElement() # <-- End ApplicationMappings

            $xmlWriter.WriteEndElement() # <-- End Application
            $appCount++ # Increment the appCount by one for use in the "ID" Application element property.
        }
    }
    $XmlWriter.WriteEndElement() # <-- End ApplicationGroup
}

$xmlWriter.WriteEndElement() # <-- End Application
$xmlWriter.WriteEndDocument() # <-- End XML Document
 
# Finish The Document
$xmlWriter.Finalize
$xmlWriter.Flush
$xmlWriter.Close()

Pop-Location

And that is all of the new code that I needed to write for SCCM/UDI. In the final part of this series, I will discuss a few SCCM/UDI quirks that were easily corrected without custom coding:
http://blog.uvm.edu/jgm/2015/03/10/sccm-udi-5-quirks/


Series Index:

Migrating to the SCCM UDI for OSD, part 3b: Operating System Selection (Continued)

Continued from part 3a:
http://blog.uvm.edu/jgm/2015/03/10/sccm-udi-3a-os/

In part 3a of this series, I provided a script that automates updating of the OS selection dialog boxes in UDI. I also noted that while UDI presents that OS selection, the UDI client ignores the selection and instead installs whichever OS image was specified at image creation time. This was very frustrating because I was able to verify that the syntax of the generated UDI XML file was correct. In examining the task sequence logs, I even could see that the “OSDImageName” and “OSDImageIndex” variables had been set as expected. Yet still UDI would not apply the select image. Why?

I found multiple explanations for this phenomenon, none in the MDT/UDI documentation:
http://kb.matthewtrotter.com/?p=94
http://www.myitforum.com/forums/SCCM-2012-MDT-2012-UDI-Image-Selection-m238011.aspx

The upshot is, while UDI wizard will set the “OSDImageName” variable, the SCCM Task Sequence ignores it by design. This issue is exposed in the Task Sequence reference:
https://technet.microsoft.com/en-us/library/hh273365.aspx

The observant reader will notice that the “Apply Operating System Image” task sequence step does not contain a variable for supplying an image name or path. You only can provide an image index. It looks like MS only supports selection of different images indexed in one monolithic WIM image. I am not enamored with this limitation, so I have authored the following script which will create one “Apply Operating System Image” for each OS in the SCCM inventory. Each step will have a condition that will allow the step to run only if the OSDImageName environment variable matches the name of the image specified in the task sequence step. Note that we are assuming only one image per WIM file in this case, so the image index of the selected WIM has to be ‘1’.

Note that this script requires the “UVM-ConfigurationManager.psm1″ module file, which can be found in this post:
http://blog.uvm.edu/jgm/2015/03/09/sccm-udi-2c-drivers/

# Update-OSApplyTaskSequence:
# Created 2015-03-09, by J. Greg Mackinnon
# Updated 2015-03-12 - Added ability to update an existing, full OS installation Task Sequence.
#                    - Also removes pre-existing "Apply Apply Operating System Image " step.  

# Script will update the SCCM Task Sequence named in the $TSPackageName variable with a conditional 
# "Apply OS Image" step for each OS currently defined in SCCM.  

# Why?  Because unless all of your OS images are contained within a single WIM file, the SCCM deployment
# sequence will ignore any OS Image selection that you make in UDI.  Instead, we will have to rely on 
# UDI to set the "OSDImageName", and we will apply OS images based on that information.  
# NOTE: We are ASSUMING one image per WIM file, so the image index value will always be set to "1".

#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_ApplyOperatingSystemAction    <-- Apply an Operating System Image

# 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

######## Begin Site-Specific Settings: ########
#Name of the TaskSequence to configure (must already exist):
[string]$TSPackageName = $name

#Name of the MDT Settings Package that contains the unattend.xml file to be used during deployment:
[string] $MDTSettingsName = 'MDT 2013 Settings'

#Name of the new Apply OS Images Group to be applied to the Task Sequence:
[string]$TSGroupName = 'UVM Apply Operating System Image Group'
######### End Site-Specific Settings: #########

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}

########################################################
########## Begin Create New OS Apply TS Group ##########
# Get the Name and PackageID for all OS image packages currently defined in SCCM, and put them into an array:
[array]$OSPackages = @()
$OSPackages = Get-WmiObject -Namespace $namespace -Query "Select Name,PackageID from SMS_ImagePackage" | Select-Object -Property Name,PackageID | Sort-Object -Property Name 
#Instantiate a new, empty TS Group:
$newTSGroup = New-SMSObject -class SMS_TaskSequence_Group
#Configure the group:
$newTSGroup.Name = $TSGroupName
$newTSGroup.Description = "Copy this group into a task sequence to replace all pre-existing 'Apply Operating System Image' actions."
#Get the PackageID for our MDT Settings Package.  We will need this later when defining the "Apply Operating System Image" TS Step:
[string] $MDTSettingsID = $(Get-WmiObject  -namespace $namespace -query "Select PackageID from SMS_Package where NAME = '$MDTSettingsName'").PackageID
foreach ($package in $OSPackages) {
    #Add an action to run each Apply Operating System Image Action:
    $ApplyOSTSAction = New-SMSObject -class SMS_TaskSequence_ApplyOperatingSystemAction
    $ApplyOSTSAction.Name = ("Apply OS Package: " + $package.name)
    $ApplyOSTSAction.ConfigFileName = "unattend.xml"
    $ApplyOSTSAction.ConfigFilePackage = $MDTSettingsID
    $ApplyOSTSAction.Description = "Conditionally applies this OS Image Package to the local drive."
    $ApplyOSTSAction.DestinationVariable = "OSDisk"
    $ApplyOSTSAction.ImagePackageID = $package.PackageID
    $ApplyOSTSAction.ImageIndex = "1"
    #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 = "OSDImageName"
    #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:
    $ApplyOSTSAction.Condition = $TSCondition #  Only one condition, not an array.
    $TSConditionExp.Dispose()
    $TSCondition.Dispose()
    $newTSGroup.Steps += @($ApplyOSTSAction)
}
########### End Create New OS Apply 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]$instIndex = Get-SMSTSStepIndex -TSObject $ExeTSGroup -TSStepName 'Install'
$instTSGroup = $ExeTSGroup.steps[$instIndex]

#Remove the existing "Apply OS Image" step, if it exists:
[string]$osApplyStepName = 'Apply Operating System Image'
if (Test-SMSTSStep -TSObject $instTSGroup -TSStepName $osApplyStepName) {
    Remove-SMSTSStep -TSObject $instTSGroup -TSStepName $osApplyStepName
}

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

#Identify the position within the task sequence group where we will add our new UVM OS Apply Group:
[int]$i = [int]$(Get-SMSTSStepIndex -TSObject $instTSGroup -TSStepName 'Set Variable for Drive Letter') + 1

#Add the new OS Apply Group to the Install Group after the position discovered in the previous step:
Add-SMSTSStep -TSObject $instTSGroup -TSStep $NewTSGroup -StepIndex $i
$newTSGroup.Dispose()

#Walk back up the task sequence tree, updating each parent group with the revised child groups:
Remove-SMSTSStep -TSObject $ExeTSGroup -TSStepName $instTSGroup.Name
Add-SMSTSStep -TSObject $ExeTSGroup -TSStep $instTSGroup -StepIndex $instIndex
$instTSGroup.Dispose()
Remove-SMSTSStep -TSObject $TS -TSStepName $ExeTSGroup.Name 
Add-SMSTSStep -TSObject $TS -TSStep $ExeTSGroup -StepIndex $exeIndex
$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()

The script will create a single group of Apply Operating System tasks (one task for each OS Image Package in the infrastructure) in the Task Sequence, using the group name supplied in the $TSPackageName variable. If the group already exists, it will be updated. Additionally, if the stock "Apply Operating System Image" step is still present, it will be removed. Each time you add or remove an OS Image, you will need to re-run the script and re-copy/paste the actions.

We have now dealt with handling Drivers and Operating Systems in SCCM/UDI OS deployment. That leaves managing UDI application selection, which I will discuss in part four of this series, coming up next.

http://blog.uvm.edu/jgm/2015/03/10/sccm-udi-4-apps/


Series Index:

Migrating to the SCCM UDI for OSD, part 3a: Operating System Selection

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

In this part of the series, I will make it possible for the end-user to select from a list of available operating system images. I will provide required scripting logic to update this selection automatically, and I will provide an additional script to make these selections work as expected. (The out-of-box UDI image selection process makes no intuitive sense at all, as we shall soon see.)

Under MDT/LTI, we used Task Sequences as the unit to control the selection of operating systems by the end user. This was necessary because the LTI wizard did not provide an operating system selection dialog. We could have authored our own OS selection dialog, but back when UVM was new to MDT (then BDD) I was a wet-behind-the-ears programmer, and programming of BDD was more difficult. However, under UDI we have the option to allow the user to select different operating systems within the UDI Wizard, so we no longer need one Task Sequence per operating system. I think that this is a positive development, although maintenance of this option proved to be more difficult than expected.

First challenge: Programmatic updating of OS Selections in the UDI Wizard

Anyone who has done experimentation with UDI surely is familiar with the UDI Designer tool. This tool provides a GUI which generates a somewhat large XML file that controls the options that are presented to UDI clients. The UDI designer allows the administrator to select the objects within SCCM that will be presented to the end-user. While this presentation granularity may be desirable for some, it presents a maintenance challenge for us. Any time an object is updated in the SCCM inventory, we need to update the UDI dialogs as well, and Microsoft provides no out-of-box means of updating these links. While this is only a minor problem for OS Image updates, it is a major hassle for Application updates. Since we really needed to solve this problem for Application updates, adding logic for operating systems was an easy extension.

To solve this problem, we need to script the reconfiguration of our UDI wizard XML file. Microsoft likes to claim that PowerShell provides “powerful” XML handling capabilities. In my experience, this claim is debatable as the built-in cmdlets have limited XML formatting capabilities. However, the .NET framework upon which PowerShell is built does provide many classes for XML handling. In this script we will be using the ” type accelerator. This accelerator represents the System.Xml.XmlDocument .NET Class, for which full documentation is available in MSDN:
https://msdn.microsoft.com/en-us/library/system.xml.xmldocument(v=vs.110).aspx

I also use the “Get-CMOperatingSystemImage” SCCM PowerShell cmdlet. As mentioned previously, I recommend avoiding the use of the cmdlets as they behave unpredictably. However, this particular cmdlets appears to work as well as we need it to. If you hate it, the commands could replaced with a WMI calls, although you would need to first discover the OS images:

$images = Get-WmiObject -namespace root/sms/site_[SiteCode] -Class SMS_ImagePackage | %{$_.__Path}

And then retrieve the “full object”, since the above query will not retrieve the required “ImageProperty” attribute (which Microsoft calls a “loosely bound” attribute):

$fullImages = @()
$fullImages += $images | % {[wmi] $_}

To use this script in your environment, you would need to update the $udiXmlIn and $udiXmlOut paths to match the desired locations of the UDIWizard_Config.xml in your environment. These paths can be the same, if desired. You also will need to update the $CMSiteCode and $CMBinPath variables.

# build-UDIImageList.ps1
# J. Greg Mackinnon
# Created: 2015-01-26
# Updated: 2015-03-17 - Added host output to indicate task progress.
# Populates the UDI Configuration Wizard XML file with all Operating System images gathered from
# Configuration Manager.  
# Requires: a local installation of the Configuration Manager administration tools.
#   Modify $udiXmlIn, $udiXmlOut, $CMSiteCode, and $CMBinPath to match your environment.

[string] $udiXmlIn = 'O:\sources\os\mdt\files\Scripts\UDIWizard_Config.xml'
[string] $udiXmlOut = 'O:\sources\os\mdt\files\Scripts\UDIWizard_Config.xml'


[string] $CMSiteCode = 'UVM'

[string] $CMBinPath = 'F:\Program Files\Microsoft Configuration Manager\AdminConsole\bin\'
[string] $CMModName = 'ConfigurationManager.psd1'
[string] $CMModPath = Join-Path -Path $CMBinPath -ChildPath $CMModName

[string] $CMDrive = $CMSiteCode + ':\'

Import-Module -Name $CMModPath | Out-Null

Push-Location $CMDrive

write-host "Gathering OS Images from SCCM..." -ForegroundColor Yellow
$osImages = Get-CMOperatingSystemImage | select -Property Name,ImageProperty

write-host "Loading the current UDI Wizard configuration file..." -ForegroundColor Yellow
1$udiXml = Get-Content $udiXmlIn
$dataElement = ($udixml.wizard.pages.page | ? -Property Name -eq 'VolumePage').data
# XPath variation, not working for some reason:
#$imgSel = $udiXml.SelectNodes("wizard/pages/page[@Name=""VolumePage""]")

#Clear the existing Nodes:
$dataElement.RemoveAll()
#Add the name/Imageselection attribute back in to the element:
$dataElement.SetAttribute('Name','ImageSelection')

write-host
foreach ($image in $osImages) {
    #Create a new DataItem element for each OS Image:
    $dataItemElement = $udiXml.CreateElement('DataItem')
    $dataElement.AppendChild($dataItemElement) | out-null

    # Read information from the existing image:
    1$imageXml = $image.ImageProperty
    [string]$ImageName = $image.Name
    [string]$Index = $imageXml.WIM.IMAGE.Index
    [string]$archNumber = ($imageXml.WIM.IMAGE.Property | ? -Property Name -eq 'Architecture').'#text'
    if ($archNumber -eq '9') {
        [string]$Architecture = 'amd64'
    } elseif ($archNumber -eq '0') {
        [string]$Architecture = 'x86'
    } else {
        [string]$Architecture = ''
    }
    #The UDI DisplayName value does not need to be tied to a property in SCCM, 
    # but we will use the matching Display Name in the SCCM GUI, which is mapped out below:
    [string]$DisplayName = $imageXml.WIM.IMAGE.name
    
    write-host "Adding image named: $DisplayName" -ForegroundColor Yellow

    #Add collected image info to a new array
    [array[]]$setters = @(
        @('DisplayName', $DisplayName),
        @('Index', $Index),
        @('Architecture', $Architecture),
        @('ImageName', $ImageName)
    )

    #Now feed data from the info array as "setter" elements under the "DataItem" element:
    foreach ($setter in $setters) {
        write-host "    Adding element: '"$setter[0]"' with property '"$setter[1]"'" -ForegroundColor cyan
        $setterElement = $udiXml.CreateElement('Setter')
        $setterElement.SetAttribute('Property',$setter[0])
        $setterElement.InnerText = $setter[1]
        $dataItemElement.AppendChild($setterElement) | out-null
    }
}

$udiXml.Save($udiXmlOut)

Pop-Location

After running this script, our UDI Wizard correctly displays all available OS Images in our environment. However, we found that the selections were not being honored by the UDI process. The fault is not in this script, but rather in the logic used by UDI. We will explore the fix for this in the next part of the series (3b).

Next: Operating Systems – Update the Task Sequence with new OS Image data
http://blog.uvm.edu/jgm/2015/03/10/sccm-udi-3b-os/


Series Index:

Migrating to the SCCM UDI for OSD, Part 2f: Driver Handling (concluded)

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

So, my driver handling routine is probably looking pretty scary right now. There are a lot of scripts that need to be run, and I have not discussed where to put them, or in which order they should be executed.  In part 6 of this series, I will provide playbook procedures for using the scripts.  Here I will provide a sys admin’s overview of where the scripts should be stored, and how they work with each other.

Script location and customization:

  1. Copy ImportDrivers.ps1, Update-DriverInjectionTaskSequence.ps1, UVM-ConfigurationManager.psm1, Create-UDIInfoFiles.ps1, and Build-UDIImageList.ps1 (see part 3) to a local directory on your Management Point or Site Server.  We use “c:\local\scripts”, with “CM-[Category]” subdirectories for each category of script.  I keep dependencies such as the “UVM-ConfigurationManager.psm1 in the “Script Root” directory.
  2. Review the starting sections for each file for local references that you might need to customize for your site.
  3. Update the path to the “UVM-ConfigurationManager.psm1″ PowerShell module in the Create-UDIInfoFiles script.
  4. Update the path to the “UVM-ConfigurationManager.psm1″ file in the Update-DriverInjectionTaskSequence script.
  5. Update the path to your MDT Files package source in the Create-UDIInfoFiles script.
  6. Copy the ZUVMDetectDriverPackage.wsf file into the “Scripts” directory of your MDT Files package.

Script sequence and dependency chains:

  1. Run “ImportDrivers.ps1″ – creates the Driver Packages and groups referenced by all future scripts and operations.  Note that the script does not distribute the packages that get created.  You must do this manually.  Anyone want to share a tip on programmatic package distribution?
  2. The remaining server side scripts can be run in any order.  After running the scripts, you must re-distribute your MDT Files to your distribution points or the UDI Task Sequence will fail with missing file or missing dependency errors.

I think that covers the procedure. I will work on streamlining the process to make it simpler to implement.

On to the handling of operating system images in UDI…

Next: Operating Systems – Update the OS Image List in the UDI Wizard:
http://blog.uvm.edu/jgm/2015/03/10/sccm-udi-3a-os/


Series Index:

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

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

In the previously explored client-side SCCM script, we saw some CSV files referenced by the client. These CSV files are not a natural part of the UDI environment, but must be generated by server-side script. What is in these files? Information about the driver packages and OS images that are defined in the SCCM database. Why not just have the client query SCCM for this information? Because to do so, we would need to inject privileged credentials into the WinPE environment, and I don’t want to do that. Creating the CSV files is a safe way to expose SCCM database info.

In authoring these posts, I see that at present the ‘zUVM-DriverCategories.csv’ gets generated twice… once when I run the ImportDrivers.ps1, and then again when running this script. Clearly this redundancy is not required. As I think about it, I probably should retire this script and merge the creation of the zUVM-OSImages.csv file into the script that updates our UDI Wizard display. I guess that will have to wait for version 2.0 of this guide.

This approach comes with the requirement that we update the CSV files any time a new OS or driver package is added to SCCM. We will get to the automation of the update process in a bit. But first, here is the UDI client CSV info file generation script, in PowerShell, of course:

# Build-UDIInfoFiles.ps1
# J. Greg Mackinnon, 2015-01-03
# Builds two files to be used by the zUVMDetectDriverPackage.wsf script that runs in WinPE during ZTI/UDI client installations.
#   YOU MUST ALSO RUN "build-UDIImageList.ps1" to ensure that the images in the UDI Wizard are identical to the images used by the zUVMSetDriverCategories script.
#   YOU MUST run this script after each run of the ImportDrivers.ps1 script.
# These are CSV files contain the following information gathered from Configuration Manager:
#   - a list of all current OS Images and a matching text string to indicate the major OS version.
#   - a list Driver Categories names, with matching Driver Category IDs.
# Requires: Configuration Manager administration tools (including CM PowerShell), and access to the SCCM server using WMI.
# Update $outFile to change the output file names ($outFile is defined twice in the script, rather unprofessionally, really).
# Update $CMSiteCode, $CMBinPath to run in a different server environment.

set-psdebug -strict

###############################################################################
######################### Start Build Driver Info File ########################
    #Build a CSV consisting of each SCCM Driver Category Name, and its corresponding UniqueID 
    [string] $computer = $env:COMPUTERNAME 
    [string] $CMSiteCode = 'UVM'

    [string] $CMBinPath = 'F:\Program Files\Microsoft Configuration Manager\AdminConsole\bin\'
    [string] $CMModName = 'ConfigurationManager.psd1'
    [string] $CMModPath = Join-Path -Path $CMBinPath -ChildPath $CMModName

    [string] $CMDrive = $CMSiteCode + ':\'

    # First output file - Driver Categories:
    [string] $outFile = 'O:\sources\os\mdt\files\Scripts\zUVM-DriverCategories.csv'

    #Cleanup existing file:
    if (Test-Path $outFile) {Remove-Item $outFile -Force -Confirm:$false}
    [string] $namespace = "root\SMS\site_" + $CMSiteCode
    
    #Note: Since we don't actually use the Category UniqueID anymore, a safer approach would be to import a list of SMS_DriverPackage objects instead:
    # Get-WmiObject -Namespace $namespace -Class SMS_DriverPackage -Property name | Select-Object -Property name | Sort-Object -Property name

    [string] $query = "select LocalizedCategoryInstanceName,CategoryInstance_UniqueID from sms_categoryinstance WHERE CategoryTypeName ='DriverCategories'"
    $wmiDriverCats = Get-WmiObject -ComputerName $computer -namespace $namespace -query $query | Sort-Object -Property LocalizedCategoryInstanceName

    #Generate CSV file with CategoryName,UniqueID:
    $driverCats = $wmiDriverCats | Select-Object -Property LocalizedCategoryInstanceName,CategoryInstance_UniqueID
    foreach ($cat in $driverCats) {
        $outStr = $cat.LocalizedCategoryInstanceName + ',' + $cat.CategoryInstance_UniqueID
        $outStr | Out-File -FilePath $outFile -Append -Encoding ascii
    }
########################## End Build Driver Info File #########################
###############################################################################


###############################################################################
########################### Start Build OS Info File ##########################
    #Second output file - OSImage information:
    [string] $outFile = 'O:\sources\os\mdt\files\Scripts\zUVM-OSImages.csv'
    if (Test-Path $outFile) {Remove-Item $outFile -Force -Confirm:$false}

    #We /could/ (and probably should) use WMI here to query root/sms/site_[siteCode]/ImagePackage 
    # (and corresponding ImagePackageInfo), but I am being lazy and will stick with the xml parsing code I already wrote:

    #Load the Configuration Manager PS module, needed for the CM cmdlet:
    Import-Module -Name $CMModPath

    Push-Location
    Set-Location $CMDrive
    $OSImages = Get-CMOperatingSystemImage
    Pop-Location

    foreach ($image in $OSImages) {
        1$imageXml = $image.ImageProperty
        $OSVer = ($imageXml.WIM.IMAGE.Property | ? -Property name -eq 'OS version').'#text'
        if ($OSVer -match '^6\.2|^6\.3') {
            [string] $winVer = 'Win8'
        } elseif ($OSVer -match '^6\.1') {
            [string] $winVer = 'Win7'
        } else {
            [string] $winVer = 'unknown'
        }
        $outStr = $image.Name + ',' + $winVer
        $OutStr | Out-File $outFile -Append -Encoding ascii
    }
############################ End Build OS Info File ###########################
###############################################################################

You may note that this code uses the Get-CMOperatingSystem PowerShell module. Earlier I said that you should not use these PS modules. I did mean it, but I wrote this code before I discovered that the module is evil, and I don’t feel like re-writing the snippet. Call me lazy.

Observant readers may have noticed in the comments the text that reads: “YOU MUST ALSO RUN ‘build-UDIImageList.ps1′ to ensure that the images in the UDI Wizard are identical to the images used by the zUVMSetDriverCategories script.” The build-UDIImageList.ps1 script is a direct component of this driver handling routine. Instead, this script is used to populate SCCM OS Images into the UDI dialogs. However, since our driver handling logic is sensitive to the OS that was selected for installation, we now have a dependency on the our UDI dialog building process. I will present this script in part 3 of this blog series, where I will discuss OS Image selection in UDI.

But first we need to wrap up our discussion of driver handling, which I will do in part 2f, next…

Next: Drivers – File Structure and Dependencies
http://blog.uvm.edu/jgm/2015/03/09/sccm-udi-2f-drivers/


Series Index:

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

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

All of the code that we have explored so far has been server-side PowerShell. In this next snippet, we will be seeing client-side VBScript that will be executed in the WinPE environment, while the task sequence is running. In part 2b, I present a Task Sequence builder script. That script created a Task Sequence step with the following command embedded:
cscript.exe %DeployRoot%\Scripts\ZUVMDetectDriverPackage.wsf

This script uses Model data generated by the “ZTIGather” script (part of MDT), and also some CSV information files that we will explore in part 2e of this post. The data is analyzed to determine which SCCM Driver Packages should be used by this client, and this information is specified in the custom TS variable “UVMDriverPackage”.

I have provided some additional logic to workaround annoying device model names like “Latitude E5440 without vPro” (the with/without vPro models use the same drivers.), and “Venue 11 Pro 7138″ (which uses the same drivers as the 7130).

When we start deploying Windows 10 OS Images, the script will need some minor updates to handle the Win10 driver pools. I already have logic for this in my production MDT/LTI client-side scripts. I just need to port that code over here.

The original code is a “wsf” file which contains XML headers and footers in addition to VBScript code. Since my WordPress instance hates in-line XML, I have had to strip these tags. The original code is available as an attachment to this post. Owing again to WordPress tomfoolery, I have had to disguise this file as a “.png”. If you wish to use the file, download it, and rename the extension to “.wsf”:

ZUVMDetectDriverPackageZUVMDetectDriverPackage.wsf

Option Explicit
RunNewInstance

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

    Function Main()
    ' //*******************************************************
    ' //
    ' // File: ZUVMDetectDriverPackage.wsf
    ' //
    ' // Purpose: Script to determine the name of the driver package to be 
    ' //   applied to the operating system in the running task sequence.  Reads
    ' //   driver category and OS Image information from files generated 
    ' //   in PowerShell.
    ' //
    ' // Usage: cscript zUVM-SetDriverCategories.wsf [/Model:ComputerModel] [/OSDImageName:WIMImageName] [/debug:true]
    ' //     Model and OSDImageName arguments are for debugging, and will override the variables in the current deployment environment.
    ' //
    ' //*******************************************************

        Dim catFile, catFilePath, csProp, dict_key, imageFile, imageFilePath, OSDImageName, Make, Model, Out, WinVer 'String variables
        Dim driverCats, OSImages, oMatch, oRegEx 'Objects
		Dim i, iRetVal

        Set driverCats = createObject("Scripting.Dictionary")
        Set OSImages = createObject("Scripting.Dictionary")
		
		'Set initial value for "UVMDriverCatSet".  We will set this to "YES" if we find a matching driver category later.
		oEnvironment.Item("UVMDriverPackageDetected") = "NO"

        catFile = "zUVM-DriverCategories.csv"
        imageFile = "zUVM-OSImages.csv"
		
		'Test to see if image/driver information files are present.  Exit if they cannot be found.
		'These two if clauses should be implemented as functions.
		iRetVal = oUtility.FindFile(catFile, catFilePath)
		if iRetVal  Success then
			oLogging.CreateEntry catFile & " file not found. ", LogTypeError
			iRetVal = Failure
			Main = iRetVal
			exit function
		end if 
		oLogging.CreateEntry "Path to " & catFile &": " & catFilePath, LogTypeInfo

		iRetVal = oUtility.FindFile(imageFile, imageFilePath)
		if iRetVal  Success then
			oLogging.CreateEntry imageFile & " file not found. ", LogTypeError
			iRetVal = Failure
			Main = iRetVal
			exit function
		end if 
		oLogging.CreateEntry "Path to " & imageFile &": " & imageFilePath, LogTypeInfo
		
        'Load the information from the files into scripting dictionary objects.
        oLogging.CreateEntry  "Loading the driver categories info file into memory...",LogTypeInfo
		call load_dict(driverCats,catFilePath)
        oLogging.CreateEntry  "Loading the OS Images info file into memory...",LogTypeInfo
        call load_dict(OSImages,imageFilePath)
		
		'The following two "If Wscript.arguments..." clauses could be implemented as functions... I'll do that when I have time (Ha!).
		'Use MDT to get Model information:
		'(Supply a valid image name in the /OSDImageName: argument, such as "Windows 8.1 Update 1 64-bit with Office 2013" for debugging)
		If Wscript.arguments.named.Exists("OSDImageName") Then
			Out = "OSDImageName argument provided on the command line. Setting OSDImageName to the supplied argument instead."
			oLogging.CreateEntry Out, LogTypeWarning
			OSDImageName =  Wscript.arguments.named.Item("OSDImageName")
		Else
			OSDImageName = oEnvironment.Item("OSDImageName")
		End If
		
		'Special handling for the OSDImageName variable... the script should not continue if OSDImageName still is not defined.
		'wscript.echo "OSDImageName is of length: " & Len(OSDImageName)
		oLogging.CreateEntry "Selected OSDImageName now is: " & OSDImageName, LogTypeInfo
		If Len(OSDImageName) = 0 Then
			oLogging.CreateEntry "OSDImageName is not defined in the MDT environment, and was not provided on the command line.  Exiting...", LogTypeError
			iRetVal = Failure
			Main = iRetVal
			exit function
		End If
		
		If Wscript.arguments.named.Exists("Model") Then
			oLogging.CreateEntry "Model argument provided on the command line.  Overriding oEnvironment setting.", LogTypeWarning
			Model =  Wscript.arguments.named.Item("Model")
		Else
			Model = oEnvironment.Item("Model")
		End If
		oLogging.CreateEntry "Model variable now is: " & Model, LogTypeInfo
        
		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(Model) then
            oLogging.CreateEntry "Model is a Latitude.  Cleaning up the model name...", LogTypeInfo
            oRegEx.pattern = " "
            set oMatch = oRegEx.Execute(Model)
            'wscript.echo "oMatch Count is: " & oMatch.count
            if oMatch.Count > 1 then
				i = oMatch.item(1).FirstIndex
				Model = Left(Model,i)
				oLogging.CreateEntry "Model is now: " & Model, LogTypeInfo
            end if
        end if
        oRegEx.pattern = "Venue 11 Pro 713"
        if oRegEx.test(Model) then
            oLogging.CreateEntry "Model is a Venue 11 Pro 713x.  Cleaning up the model name...", LogTypeInfo
            oRegEx.pattern = "713"
            set oMatch = oRegEx.Execute(Model)
            'wscript.echo "oMatch Count is: " & oMatch.count
            i = oMatch.item(0).FirstIndex
            'wscript.echo "index of match is: " & i
            Model = Left(Model,i+3)
            oLogging.CreateEntry "Model is now: " & Model, LogTypeInfo
        end if
		
		'Lookup the Windows version string for this image from the OSImages dictionary (loaded from file).
		'Exit if the image is not listed.
        If OSImages.Exists(OSDImageName) Then
            WinVer = OSImages.Item(OSDImageName)
        Else
			oLogging.CreateEntry "Selected image was not found in the CSV list of available images.", LogTypeError
			oLogging.CreateEntry "Searched for: " & OSDImageName, LogTypeError
			iRetVal = Failure
			Main = iRetVal
            exit function
        End If

        dict_key = WinVer & "-" & Model
		oLogging.CreateEntry "Looking for driver category entry: " & dict_key, LogTypeInfo
        If driverCats.Exists(dict_key) Then
            'Lookup the driver category ID string from the driverCats dictionary (loaded from file):
			Out = CStr("Found entry for " & dict_key)
            oLogging.CreateEntry Out, LogTypeInfo
            oEnvironment.Item("UVMDriverPackageDetected") = "YES"
            oEnvironment.Item("UVMDriverPackage") = dict_key
        'If we are dealing with a Windows 8 image, then check for Windows 7 drivers when Win8 drivers can't be found:
        ' (Note: In MDT/LTI I had logic to set a "MaxOS" version, and a For or While loop to count down to the lowest supported OS.
        '  We should do that here, too.)
        ElseIf InStr(WinVer,"Win8") Then
			Out = "No entry found for " & dict_key & " in the Windows 8 driver categories.  Now looking in the Windows 7 driver categories."
			oLogging.CreateEntry Out, LogTypeWarning
            WinVer = "Win7"
            dict_key = WinVer & "-" & Model
			If driverCats.Exists(dict_key) Then
				Out =  CStr("Found entry for " & dict_key)
                oEnvironment.Item("UVMDriverPackageDetected") = "YES"
                oEnvironment.Item("UVMDriverPackage") = dict_key
				oLogging.CreateEntry Out, LogTypeInfo
			Else 
				Out = "No entry found for this model. Leaving UVMDriverPackageDetected set to 'NO'."
				oLogging.CreateEntry Out, LogTypeInfo
			End If
        Else
			Out = "No entry found for this model. Leaving UVMDriverPackageDetected set to 'NO'."
			oLogging.CreateEntry Out, LogTypeInfo
        End If
		
		iRetVal = Success
		Main = iRetVal

    End Function

    Function load_dict(dict_name,file_name)
        Dim objFSO,objText,line,pair_array,index,item
        Const ForReading = 1, ForWriting = 2, ForAppending = 8, ReadOnly = 1

        Set objFSO = CreateObject("Scripting.FileSystemObject")  'Create file object
        oLogging.CreateEntry "Opening the script dictionary source file '" &  file_name & "'.",LogTypeInfo
        Set objText = objFSO.OpenTextFile(file_name, ForReading) 'Open for read

        Do Until objText.AtEndOfStream        'Read to end of file
            line = objText.ReadLine                  'Read line from file
            'Uncomment for debugging:
            'oLogging.CreateEntry "Loading '" & line &"'.",LogTypeInfo
            pair_array = split(line,",")             'Split Index-Item pair
            index = pair_array(0) 'Decode Index
            item  = pair_array(1) 'Decode Item
            dict_name.Add index,item  'Add to Dictionary
        Loop                                      'Read another line
        objText.Close
    End Function

End Class

In part 2E of this post, I will discuss the final bits of server-side code required to generate the CSV information files used by this client-side script…

Next: Drivers – Providing SCCM database info to clients
http://blog.uvm.edu/jgm/2015/03/09/sccm-udi-2e-drivers/


Series Index:

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: