A little corner of the Empire on the web.

16 April, 2019

Retrieving Package Source Path/Application Content Locations from SCCM

We've been running with an old file server and a new file server for our SCCM package sources for a while now, with all the new software being put on the new server, and obsolete software being deleted from the old server as it goes out of use. Over time the old server should be used less and less as a package source, until it falls out of use and is decommissioned, but we really need to get a shift on with getting everything off the old hardware, and onto the new one. To make things easier for future moves the new one's referenced in all our systems with a fairly generic DNS name (rather than an old and obscure machine naming scheme), which should also make future server changes easier to manage.

So the first thing that I need to do is trundle through SCCM and work out what software is still using the old server as its source. There are a few main categories of bulk content that could be referencing files on that old server:

  1. Programs (using the Package Model)
  2. Driver Packages
  3. Applications (using the Application model)

Any of those could have their content files stored on either server. Packages are fairly easy as you can add a column to the console called "Package Source Path" and then sort by that. Applications are not so easy; each Deployment Type within an app has its own individual source path, and you have to drill right into the Deployment Type's properties to see it, there's no quick and easy way to see a list. (Obviously things like OS Images, Boot Images, etc could also be on the old server but we've only got single digit numbers of each of those, so they're just as quick to check one by one).

Packages (and Programs)

First the easy options, Packages and Driver Packages. Packages have one Package Source Path for the whole Package, which is used by each Program in the Package. All we need to do is use a cmdlet to retrieve all of the packages, then just pull out the properties from each that we're interested in, and finally dump that to a file that we can open in Excel and work our way through:

(All of the below PowerShell commands to be executed from a Config Manager PowerShell window, or a normal PowerShell command prompt with the ConfigurationManager module loaded and the current location changed to the Config Manager PSDrive for your site).

PS ABC:\> $Packages = Get-CMPackage
PS ABC:\> $Packages | Select-Object -Property PackageID, Name, PkgSourcePath | Export-Csv -Path C:\temp\PkgSrc.csv -NoTypeInformation

Driver Packages

These are very, very similar to normal packages. So similar that you may not even notice there's six extra letters in the initial cmdlet.

PS ABC:\> $DriverPackages = Get-CMDriverPackage
PS ABC:\> $DriverPackages | Select-Object -Property PackageId, Name, PkgSourcePath | Export-Csv -Path c:\Temp\DriverPkgSrc.csv -NoTypeInformation

Both of the above (Get-CMPackage and Get-CMDriverPackage) are relatively quick, they took less than a minute even running them from a remote machine against a site with a large number of [driver] packages .

Application model applications

Ok, here we go, here's the big one. Each Application can have multiple Deployment Types, each of those deployment types (which can each use a different deployment style) has its own, individual Content Location. To add to this, these queries are slower than their equivalents in the Package model (we're talking multiple minutes rather than seconds), long enough that there's some progress text output during this one, just to let you know that it's actually doing something. Then finally details of the app's Deployment Type(s) are all listed in a big XML property of the Application object, rather than each being their own object properties.

Here we are. Firstly we get the full list of apps from the site (which is the part that I've seen take 10 to 15 minutes on a site with around 800 app model apps), then iterate over them, pulling the relevant properties out of the XML (including iterating over each Deployment Type in that app), then finally output all of that as an object which we write to a CSV file:

$CSVOutPath = 'c:\temp\AppSrc.csv'
#Change to your site's ConfigMgr PSDrive before running this script

#Get each Application, then get the source path from each Applications's Deployment Types
Write-Host "Fetching application details - this takes a few minutes, please wait...`n"

$Applications = Get-CMApplication
$AppCount = $Applications.Count
Write-Host "$AppCount applications found.`n"

#Iterate over the apps list pulling out the details for each app. Takes a couple of minutes.
$i = 1
$AppSourceList = ForEach($App in $Applications)
    Write-Progress -Activity 'Checking apps and deployment types' -Id 1 -PercentComplete $(($i / $AppCount) * 100) -CurrentOperation "app $i / $AppCount"
    $PackageXml = [xml]$App.SDMPackageXML
    #An app can have multiple Deployment Types, each with their own source location. DT details are stored in the XML properties
    ForEach($DT in $PackageXml.AppMgmtDigest.DeploymentType) {
        $DtTitle = $DT.Title.'#text'    #need to quote property names with hashes in them, normal backtick escaping doesn't work
        $DtTech = $DT.Technology
        $DtLocation = $DT.Installer.Contents.Content.Location
        New-Object -TypeName psobject -Property (@{AppDisplayName = $App.LocalizedDisplayName; PackageID = $App.PackageID;
        CiId = $app.CI_ID; Enabled = $app.IsEnabled; Superseded = $app.IsSuperseded; HasContent = $app.HasContent;
        DepTitle = $DtTitle; DepTypeTech = $DtTech; DepTypeSrcLocation = $DtLocation})

#$AppSourceList | Out-GridView
$AppSourceList | Export-Csv -Path $CSVOutPath -NoTypeInformation

Download the Get-ApplicationSources.ps1 here: GitHub Get-ApplicationSources.ps1

(No download for the Package and Driver Package sources, as they're basically just one-liners).

09 April, 2019

SCCM Detection Rules for Dell BIOS Updates

I've been doing a bit of work lately on deploying Dell BIOS updates using SCCM. This is using SCCM's Application Model to deploy the updates out to live machines, and also using those to update BIOS firmware during OSD Task Sequences. I've been a through a few versions of SCCM app detection rules for these jobs. The problem is that WMI's SMBIOSBIOSVersion is a text field, so it's returned as and then treated as a string type. There's also nothing that standardises how manufacturers fill in that text field, so they can use whatever versioning scheme they want, with whatever text they want around it. So different manufacturers do it different ways and they often change it between models.

Dell (at least for their Optiplex and Latitude business products) seem to use two different BIOS version number schemes at the moment, they seem to use one on older models, and the other for newer models, and they each have their own challenges. The first scheme uses an "A" followed by a two digit integer, counting up from zero, eg "A00" followed by "A01" then "A02" with "A09", "A10", "A11" being later in the series. The second scheme is a three part version number separated by dots (full stops, periods whatever your region calls them), eg "1.0.0", then "1.1.0" and later releases may be "1.6.2", "1.9.3", "1.11.2".

Taking them one by one, the problem with the "A00" scheme is that it's a mix of letters and numbers, so you can't do a purely numeric comparison to get the larger number, but at least a string greaterThanEqualTo (-ge) comparison of them does work, and you can always remove the leading "A" and then just do a numerical comparison.

Unfortunately the "1.0.0" type scheme works perfectly with a string greaterThanEqualTo (-ge) comparison right up to point where any of the numbers in it become double digits, so '1.2.0' -ge '1.0.5' returns True, whereas '1.12.0' -ge '1.3.5' unexpectedly returns False. This is because it's comparing each digit in the number one by one, so the first digit "1" is the same in both, then the dot is the same in both, then "1" is less than "3", so the greater than test fails, even though 12 is bigger than 3 it only looks at the digits individually.

The end result of this is that I'm now using the following PowerShell script in a script detection rule for Dell's BIOS updates. You just put the version number of the BIOS version that you're deploying in the variable in the first line then it parses it all correctly from that.

$NewBiosVer = '1.13.0'
$CurrentBIOSVer = (Get-WmiObject -Class Win32_BIOS).SMBIOSBIOSVersion
#Check if this is old-style Dell BIOS versioning "Ann" or new-style "n.n.n"
If ($NewBIOSVer.StartsWith('A') -and $CurrentBIOSVer.StartsWith('A')) {
    If ($CurrentBIOSVer -ge $NewBIOSVer) {
        #To indicate software is found, return something in STDOUT
        Write-Host "BIOS Version $CurrentBIOSVer found"
    } else {
        #To indicate software not found, don't return anything to STDOUT or STDERR
} ElseIf (($NewBiosVer -match "^[\d\.]+$") -and ($CurrentBIOSVer -match "^[\d\.]+$")) {
    #Check if we've got a problem with two digit decimals (where 1.9 is less than 1.10 in version numbers, but not numerically)
    $NewBiosVerArray = $NewBiosVer.Split('.')
    $CurrentBIOSVerArray = $CurrentBIOSVer.Split('.')
    If ([int]$CurrentBIOSVerArray[0] -ge [int]$NewBiosVerArray[0]) {
        If ([int]$CurrentBIOSVerArray[1] -ge [int]$NewBiosVerArray[1]) {
            #To indicate software is found, return something in STDOUT
            Write-Host "BIOS Version $CurrentBIOSVer found"
        Else {
            #To indicate software not found, don't return anything to STDOUT or STDERR
    } else {
        #To indicate software not found, don't return anything to STDOUT or STDERR
} Else {
    #Doesn't fit either BIOS version format???
    #To indicate software not found, don't return anything to STDOUT or STDERR

For the task sequence step's run conditions I'm doing two WQL queries, one for the device's model, and the second a simple check that the BIOS version is not the one that the update's looking for (so that machines on our nominated version skip the step) and then the Application's detection rules check fully whether the currently installed version is older or newer than the one we're installing (using the above script).