Sunday, February 16, 2014

Identify the only drivers a machine needs for the OSD driver import process in Configuration Manager

Last week I talked about using a regex expression to find a syntax error in a directory containing all of my scripts. This week, I'll use the same method to find all of the driver INF files, needed for a given machine. This will allow me to provide a specific list of driver INF files to use for importing into Configuration Manager.


The Problem

One of the more tedious aspects of preparing new hardware for OS deployment with Configuration Manager is the driver import process. The process involves identifying the driver, downloading or extracting it to a central location, and then importing it. Rinse and repeat until all of the exclamation marks disappear in the Device Manager. This can make for a long morning and the temptation to grab a full driver cab and import the whole thing and be done with it is great. But then you are left with a bloated driver package where most of the drivers aren't even required. 

The Solution

I can't help you avoid having to download a bunch of drivers but with the help of the script discussed here, we can identify precisely what your computer needs and only import those drivers. Thus creating a much smaller and manageable driver store. 

In order to do this, the script needs to:
  1. Identify all of the hardware Names and DeciveIDs in Win32_PNPEntity
  2. Toggle the ability to only search for unknown devices
  3. Extract a string from the DeviceID to be used as our regex espression
  4. Recursively search a given directory for INF files that contain our regex expression
  5. Return a device object with an array of INF file locations for each found device INF
Here's the output:
PS C:\> & "D:\Sandbox\Driver_INF_File_Search\Driver_INF_File_Search.ps1" C:\Dell
DeviceName : Intel(R) Mobile Express Chipset SATA RAID Controller SearchPattern : DEV_282A FileArray : {C:\Dell\E6530\win8\x64\storage\2PWDK_A01-00\Drivers\AHCI\x64\iaStorAC.inf, C:\Dell\E6530\win8\x86\storage\2PWDK_A01-00\Drivers\AHCI\x32\iaStorAC.inf}

Preparation

The one caveat to this process is that in order to be able to search for the driver, it must be present in the given directory or subdirectory. This means we'll still have to download the drivers manually before we use the script.
Some vendors provide full driver cabs for their machines. If your vendor provides them, download and extract them to the file system, then use the script to identify only those that your machine needs. 

Querying Win32_PNPEntity

To get the deviceIDs we use the Win32_PNPEntity class in WMI. Using this class also allows us to find devices that are marked unknown through the use of the ConfigManagerErrorCode property. The WMI query isn't terribly complex so let's move on to the file search routine.

File search routine

Once we get the list of IDs to search for we need to extract a string from them to use for the search because there is no place in the INF file that contains the entire deviceID string. On top of that, there are a few different forms that the ID can take. This script manages to find IDs that contain "&DEV" and "DLL". I haven't worked out the other possibilities but these two values cover a lot of ground.

The following routine utilizes regex to first find an eligible value to extract a string from and then uses another regex to identify the sting to extract. It then assigns that string to the $Pattern variable to be used in the select-string statement. All matched deviceIDs are then searched for, discarding those that were not modified by the regex statements.

#--Assign the deviceid to the pattern for evaluation            
$Pattern = $Device.DeviceID            
                
#--Evaluate $Pattern for occurrence of "&DEV" or "DLL"            
switch -regex ($Pattern) {            
   "&DEV" {$Pattern -match 'DEV_\w+[^&]' | out-null;$PatternModified = $True}
   "DLL"  {$Pattern -match 'DLL\w+[^\\]' | out-null;$PatternModified = $True} 
}           
#--Search directory for INF files with the occurance of $Pattern            
If ($PatternModified) {            
   #--Set new regex pattern            
   $Pattern = $matches[0]            
   #--Find files that contain the extracted pattern            
   $Result = Get-ChildItem $Directory -include *.inf -recurse | select-string -pattern $Pattern | Select-Object -Unique Path            
}

Returning unique file locations

While the derived search term is very successful in being found, it occurs in the INF file many times. This means that every time it is found, it will be returned to the query resulting in many references to the same file for a single device. We only want to return unique file locations. The way we accomplish this is by piping the returned result through the Select-Object cmdlet like this:
Select-Object -Unique Path
In preparation for writing an automated import process for Configuration Manager, I return objects rather than writing the results to the host. "When in doubt, use PSobjects"  - Abe Lincoln

That's it for this week. Hopefully some Configuration Manager admins will find this method useful in taming the driver beast.

Here is a link to the script on Github:
Driver_INF_File_Search.ps1 

And here is a link to the script on the Technet gallery:

http://gallery.technet.microsoft.com/Driver-Inf-File-Search-for-fe8a95cc


The Script

#========================================================================            
# Date              : 2/16/2014 4:49 PM            
# Author            : Jeff Pollock            
# Website           : http://lifeinpowershell.blogspot.com/            
#             
# Description       : Assists in identifying driver INF files for import            
#                     into Configuration Manager 2007 & 2012. It does this            
#                     by searching a given computer for all PNP devices            
#                     and then searching a given directory for the related            
#                     INF files.             
#========================================================================            
Param (            
    [parameter(Mandatory=$true,ValueFromPipeline=$True)]            
    [string]$SearchDir,  #Directory to search            
            
    [parameter(ValueFromPipeline=$True)]            
    [string]$Computer = ".",  #Computer to search            
            
    [parameter(ValueFromPipeline=$True)]            
    [switch]$UnknownOnly  #Determines whether to return all devices or just unknown            
)            
            
#----------------------------------------------            
#region Functions            
#----------------------------------------------            
Function Get-PNPDevices {            
    [cmdletbinding()]            
    Param (            
        [bool]$UnknownOnly  #-determines whether to return all devices or just unknown                  
    )            
            
    #--Query Win32_PNPEntity for devices            
    If (!$UnknownOnly) {            
        $Devices = Get-WmiObject -ComputerName $Computer Win32_PNPEntity |
        Select Name, DeviceID            
    } Else {            
        $Devices = Get-WmiObject -ComputerName $Computer Win32_PNPEntity |
        Where-Object{$_.ConfigManagerErrorCode -ne 0} | Select Name, DeviceID    
    }            
            
    #--Return device objects            
    ForEach ($Device in $Devices) {            
        $DeviceObj = New-Object -Type PSObject            
        $DeviceObj | Add-Member -MemberType NoteProperty -Force -Name DeviceName -Value $Device.Name
        $DeviceObj | Add-Member -MemberType NoteProperty -Force -Name DeviceID -Value $Device.DeviceID
        $DeviceObj                    
    }            
}            
            
Function Search-DeviceINF {            
    [cmdletbinding()]            
 Param(            
     [parameter(Mandatory=$true,ValueFromPipeline=$True)]            
     [string]$Directory,  #Directory to search

     [parameter(Mandatory=$true,ValueFromPipeline=$True)]            
     [object]$Device  #Device object            
 )            
                
    #--Create array to hold returned file names            
    $FileArray = @()            
                
    #--Assign the deviceid to the pattern for evaluation            
    $Pattern = $Device.DeviceID            
                
    #--Evaluate $Pattern for occurance of "&DEV" or "DLL"            
    switch -regex ($Pattern) {            
        "&DEV" {$Pattern -match 'DEV_\w+[^&]' | out-null;$PatternModified = $True}            
        "DLL"  {$Pattern -match 'DLL\w+[^\\]' | out-null;$PatternModified = $True}            
    }            
            
    #--Search directory for INF files with the occurance of $Pattern            
    If ($PatternModified) {            
        #--Set new regex pattern            
        $Pattern = $matches[0]            
        #--Find files that contain the extracted pattern            
        $Result = Get-ChildItem $Directory -include *.inf -recurse |
        select-string -pattern $Pattern | Select-Object -Unique Path            
    }            
            
    #--Output results            
    If($Result) {            
        #--Add returned files to the FileArray            
        $Result | ForEach-Object {$FileArray += $_.Path}            
            
        #Create object and output            
        $DeviceObj = New-Object -Type PSObject            
        $DeviceObj | Add-Member -MemberType NoteProperty -Force -Name DeviceName -Value $Device.DeviceName
        $DeviceObj | Add-Member -MemberType NoteProperty -Force -Name SearchPattern -Value $Pattern
        $DeviceObj | Add-Member -MemberType NoteProperty -Force -Name FileArray -Value $FileArray            
        $DeviceObj | format-list            
    }             
}            
#endregion Application Functions            
            
#----------------------------------------------            
# region Script            
#----------------------------------------------            
#--Set Unknown switch            
If ($UnknownOnly) {            
    [bool]$Unknown = $True            
} Else {            
    [bool]$Unknown = $False            
}            
            
#--Perform query            
Get-PNPDevices $Unknown | ForEach-Object {              
   Search-DeviceINF $SearchDir $_             
}            
#endregion Script

Monday, February 10, 2014

Running an older script in PowerShell 3 or 4 generates the following error id: InvalidVariableReferenceWithDrive

Today's post deals with a problem in a few of my scripts that I encountered after upgrading to PowerShell 3 from version 2. We'll discuss both the problem and the simple regex pattern I used to identify where it may have existed in all of our other scripts. 

The problem has to do with variables used inside a string, followed immediately by a colon. Say what? Read on…

The below code is interpreted correctly in PowerShell 1 & 2 but in PowerShell 3 and 4 it is expecting a mount point or a “drive” immediately following the colon. 

Example:
log "Installing Beyond Compare $exeversion:"
log "$exeversion: Already Installed."

Error:
+         log "Installing Beyond Compare $exeversion:"
+                                        ~~~~~~~~~~~~
Variable reference is not valid. ':' was not followed by a valid variable name character. Consider using ${} to delimit the name.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : InvalidVariableReferenceWithDrive

To correct this, the line needs to be updated to encapsulate the variable so it is processed first and then returned to the string.

log "Installing Beyond Compare $($exeversion):”
log "$($exeversion): Already Installed."

You probably already know whether you've coded a string using a variable this way or not but we weren't sure how prevalent it was in our organization. 

Finding the errant code:
So now that we were aware of the problem, we had to identify all of our scripts that contained the errant code conditions. In essence we had to do the following:

  1. Get all occurrences of our "*.ps1" script files with a recursive child item query.
  2. Search each script for the occurance of our regex pattern.
  3. Display the full file name, line number, and string

Here is the command line we used to do just that and its resulting output.

PS C:\windows\system32> Get-ChildItem \\server\share -include *.ps1 -recurse | select-string -pattern '\".*\$\w+\:[\s*\"]'


\\server\share\Adobe\script\CommonFunctions.ps1:2864:            } Else {Write-Output "$FontFile: Already present"}    
\\server\share\Scooter Software\3.3.5\setup.ps1:161:        log "Installing Beyond Compare $exeversion:"
\\server\share\SunGard\Fonts\CommonFunctions.ps1:2864:            } Else {Write-Output "$FontFile: Already present"}

In a repository with 100's of scripts, only 3 presented this unique condition. The output of this query contains the file location, line number, and code line that needs to be corrected. At this point it wouldn't take much effort to automate the correction of the code as well but hey, I wouldn't want to steal all the fun!

What's truly great about this method is that you can search any text readable file for anything! In a later post, I'll show you how I used this search method to tame the driver store beast in SCCM.

Breaking down the regex:
As promised, here's the breakdown of the regular expression I used.

'\".*\$\w+\:[\s*\"]'

\"          - Expression must begin with a quote
.*           - Any amount of characters
\$           - The dollar sign or beginning of the broken variable we are looking for
\w+        - A series of characters with no spaces
\:             - the colon or end of the broken variable we are looking for
[\s*\"]   - A space followed by anything and then a quote

For an excellent PowerShell regex reference, check out this link: 
PowerShell Cookbook: Regular Expression Reference

Thanks for checking out this article. I hope you find it informative. Until next time...

Thursday, February 6, 2014

Welcome to Life in Powershell

Welcome to Life in Powershell

Welcome to "Life in PowerShell" where I'll be writing about some of the scripts and techniques I use on a daily basis. I've been using PowerShell for about two years and the many blogs and contributions by the PowerShell community have been very helpful to me along the way. This blog is my attempt at giving back to the community. 

In my professional life I am the lead administrator for System Center Configuration Manager at a large insurance company. Serving in this role provides me many unique challenges and opportunities. PowerShell is the main tool I use to meet these challenges. Since most of my time is spent in ConfigMgr, many of my posts will revolve around it as well. I hope you find this blog informative. 

Well that's it for my introduction. Now to come up with the first topic...